# coding=utf-8
from __future__ import absolute_import
from __future__ import unicode_literals

import json
import sys

import plexnet
from kodi_six import xbmc
from kodi_six import xbmcgui
from kodi_six import xbmcaddon
from threading import Timer

import lib.cache
from lib import util
from lib import genres
from lib import actions
from lib.util import T
from . import kodigui
from . import windowutils


UNDEF = "__UNDEF__"


class Setting(object):
    type = None
    ID = None
    label = None
    desc = None
    default = None
    userAware = False
    isThemeRelevant = False
    backport_from = None

    def translate(self, val):
        return str(val)

    def get(self, *args, **kwargs):
        _id = kwargs.pop("_id", UNDEF)
        use_id = _id if _id != UNDEF else self.ID
        default = kwargs.pop("default", UNDEF)
        use_default = default if default != UNDEF else self.default

        value = util.getSetting(use_id, use_default)
        if value == use_default and self.backport_from:
            # fallback set and we're on default
            old_val = util.getSetting(self.backport_from, DEFAULT)

            # old setting was set
            if old_val != DEFAULT:
                # get correct old value
                old_val_cast = util.getSetting(self.backport_from, use_default)
                util.setSetting(self.backport_from, '')

                # old value is different from the new one, set
                if old_val_cast != use_default:
                    self.set(old_val_cast, skip_get=True)
                    value = old_val_cast
        return value

    def emit_events(self, id_, val, **kwargs):
        plexnet.util.APP.trigger('change:{0}'.format(id_), value=val)

    def emit_tr_events(self, id_, val, **kwargs):
        plexnet.util.APP.trigger('theme_relevant_setting', id=id_, value=val, **kwargs)

    def set(self, val, skip_get=False):
        if not skip_get:
            old = Setting.get(self)
            setRet = util.setSetting(self.ID, val)
            if old != val:
                util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]', self.ID, old, val)
                self.emit_events(self.ID, val)
                if self.isThemeRelevant:
                    self.emit_tr_events(self.ID, val)
        else:
            setRet = util.setSetting(self.ID, val)
        return setRet

    def valueLabel(self):
        return self.translate(self.get())

    def __repr__(self):
        return '<Setting {0}={1}>'.format(self.ID, self.get())


class BasicSetting(Setting):
    def __init__(self, ID, label, default, desc='', theme_relevant=False, backport_from=None):
        self.ID = ID
        self.label = label
        self.default = default
        self.desc = desc
        self.isThemeRelevant = theme_relevant
        self.backport_from = backport_from

    def description(self, desc):
        self.desc = desc
        return self


class ListSetting(BasicSetting):
    type = 'LIST'
    options = ()

    def translate(self, val):
        return self.options[len(self.options) - 1 - val]

    def optionLabels(self):
        return self.options

    def optionIndex(self):
        return len(self.options) - 1 - self.get()

    def set(self, val, skip_get=False):
        BasicSetting.set(self, len(self.options) - 1 - val, skip_get=skip_get)


class QualitySetting(ListSetting):
    options = (
        T(32001),
        T(32002),
        T(32003),
        T(32004),
        T(32005),
        T(32006),
        T(32007),
        T(32008),
        T(32009),
        T(32010),
        T(32011),
        T(32012),
        T(32013),
        T(32014),
    )


class ThemeMusicSetting(ListSetting):
    options = [
        T(32481),
    ] + [T(32482) % {"percentage": 10+i} for i in range(0, 100, 10)]


class PlayedThresholdSetting(ListSetting):
    options = ['{} %'.format(perc) for perc in range(70, 100, 5)]


class BoolSetting(BasicSetting):
    type = 'BOOL'


class UserAwareSetting(BasicSetting):
    """
    A user-aware BoolSetting
    """
    userAware = True

    def __init__(self, *args, **kwargs):
        super(UserAwareSetting, self).__init__(*args, **kwargs)
        util.USER_SETTINGS.append(self.ID)

    @property
    def userAwareID(self):
        if plexnet.plexapp.ACCOUNT and plexnet.plexapp.ACCOUNT.ID:
            return '{}.{}'.format(self.ID, plexnet.plexapp.ACCOUNT.ID)
        return 'USER_AWARE'

    def emit_events(self, id_, val, **kwargs):
        plexnet.util.APP.trigger('change:{0}'.format(self.ID), key=self.userAwareID, value=val, skey=self.ID)

    def get(self, *args, **kwargs):
        _id = kwargs.pop("_id", UNDEF)
        default = kwargs.pop("default", UNDEF)
        return super(UserAwareSetting, self).get(*args,
                                                 _id=_id if _id != UNDEF else self.userAwareID, default=default,
                                                 **kwargs)

    def set(self, val, skip_get=False):
        if not skip_get:
            old = self.get(_id=self.userAwareID)
            if old != val:
                util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]', self.userAwareID, old, val)
                self.emit_events(self.userAwareID, val)
                if self.isThemeRelevant:
                    self.emit_tr_events(self.userAwareID, val)
        return util.setSetting(self.userAwareID, val)


class BoolUserSetting(UserAwareSetting, BoolSetting):
    pass


class OptionsSetting(BasicSetting):
    type = 'OPTIONS'

    def __init__(self, ID, label, default, options, **kwargs):
        BasicSetting.__init__(self, ID, label, default, **kwargs)
        self.options = options

    def translate(self, val):
        for ID, label in self.options:
            if ID == val:
                return label

    def optionLabels(self):
        return [o[1] for o in self.options]

    def optionIndex(self):
        val = self.get()
        for i, o in enumerate(self.options):
            if val == o[0]:
                return i

        return 0


DEFAULT = "__DEFAULT__"


class MultiOptionsSetting(OptionsSetting):
    type = 'MULTI'
    valueLabelDelim = ', '
    noneOption = None

    def __init__(self, ID, label, default, options, none_option=None, **kwargs):
        super(MultiOptionsSetting, self).__init__(ID, label, [], options, **kwargs)
        self.default = default
        self.noneOption = none_option
        util.JSON_SETTINGS.append(self.ID)

    def get(self, *args, **kwargs):
        with_default = kwargs.pop('with_default', True)
        val = super(MultiOptionsSetting, self).get(*args, default=DEFAULT, **kwargs)
        if val and val != DEFAULT:
            try:
                return json.loads(val)
            except json.decoder.JSONDecodeError:
                # fallback for legacy settings that weren't json
                for o in self.options:
                    if o[0] == val:
                        return [val]
        elif val == DEFAULT:
            # backport old separated options
            ret = self.default[:]
            for o in self.options:
                lval = util.getSetting(o[0], DEFAULT)
                # we only support backporting booleans right now
                if lval != DEFAULT:
                    if lval == "true" and o[0] not in self.default:
                        ret.append(o[0])
                    elif lval == "false" and o[0] in self.default:
                        ret.remove(o[0])

            if ret != self.default:
                self.set(ret, skip_get=True)
                return ret
        return with_default and self.default or []

    def set(self, val, skip_get=False):
        super(MultiOptionsSetting, self).set(json.dumps(val), skip_get=skip_get)

    def translate(self, val, return_str=False, delim=", "):
        if isinstance(val, (list, tuple, set)):
            # keep options order
            data = [super(MultiOptionsSetting, self).translate(o[0]) for o in self.options if o[0] in val]
            if return_str:
                return delim.join(data)
            return data
        return super(MultiOptionsSetting, self).translate(val)

    def valueLabel(self, values=None):
        vals = values or self.get()
        if vals:
            return self.translate(vals, return_str=True, delim=self.valueLabelDelim)
        return T(33056, "None")

    def optionIndex(self):
        val = self.get()
        ret = []
        for i, o in enumerate(self.options):
            if o[0] in val:
                ret.append(i)
        return ret


class MultiUAOptionsSetting(MultiOptionsSetting, UserAwareSetting):
    pass


class KCMSetting(OptionsSetting):
    key = None

    def emit_events(self, id_, val, **kwargs):
        plexnet.util.APP.trigger('change:{0}'.format(self.ID), value=val)

    def get(self, *args, **kwargs):
        return getattr(lib.cache.kcm, self.key)

    def set(self, val, skip_get=False):
        if not skip_get:
            old = self.get()
            if old != val:
                util.DEBUG_LOG('Setting: {0} - changed from [{1}] to [{2}]', self.ID, old, val)
                self.emit_events(self.ID, val)

        lib.cache.kcm.write(**{self.key: val})


class BufferSetting(KCMSetting):
    key = "memorySize"


class ReadFactorSetting(KCMSetting):
    key = "readFactor"


class InfoSetting(BasicSetting):
    type = 'INFO'

    def __init__(self, ID, label, info):
        BasicSetting.__init__(self, ID, label, None)
        self.info = info

    def valueLabel(self):
        return self.info


class PlatformSetting(InfoSetting):
    def __init__(self):
        InfoSetting.__init__(self, None, None, None)
        self.ID = 'platfom_version'
        self.label = T(32410, 'Platform Version')

    def valueLabel(self):
        plat = None
        try:
            if sys.version_info[0] >= 3:
                from lib import distro
                dist = distro.linux_distribution()
                if dist and len(dist) > 1:
                    plat = u'{0} {1}'.format(dist[0], dist[1])
            else:
                import platform
                dist = platform. dist()
                if dist and len(dist) > 1:
                   plat = u'{0} {1}'.format(dist[0], dist[1])
                else:
                   plat = platform.platform()
                   plat = u'{0} {1}'.format(plat[0], '.'.join(plat[1].split('.', 2)[:2]))
        except:
           util.ERROR()

        plat = plat.strip()

        if not plat:
            if xbmc.getCondVisibility('System.Platform.Android'):
                plat = 'Android'
            elif xbmc.getCondVisibility('System.Platform.OSX'):
                plat = 'OSX'
            elif xbmc.getCondVisibility('System.Platform.Darwin'):
                plat = 'Darwin'
            elif xbmc.getCondVisibility('System.Platform.Linux.RaspberryPi'):
                plat = 'Linux (RPi)'
            elif xbmc.getCondVisibility('System.Platform.Linux'):
                plat = 'Linux'
            elif xbmc.getCondVisibility('System.Platform.Windows'):
                plat = 'Windows'

        return plat or T(32411, 'Unknown')


class ServerVersionSetting(InfoSetting):
    def valueLabel(self):
        if not plexnet.plexapp.SERVERMANAGER.selectedServer:
            return ''

        return plexnet.plexapp.SERVERMANAGER.selectedServer.rawVersion or ''


class IPSetting(BasicSetting):
    type = 'IP'


class IntegerSetting(BasicSetting):
    type = 'INTEGER'


class KeySetting(BasicSetting):
    type = 'STRING'

    def value_setter(self):
        w = SchnorchelDialog()
        timeout = Timer(w.timeout, w.close)
        timeout.start()
        w.doModal()
        timeout.cancel()
        choice = w.key
        del w
        return choice

    def get(self, as_code=False, *args, **kwargs):
        code = super(KeySetting, self).get(default=None, *args, **kwargs)
        if as_code:
            return code

        if code is not None and code != "None":
            ak = actions.ActionKey(int(code))
            return ak


class Settings(object):
    SETTINGS = {
        'main': (
            T(32000, 'Main'), (
                BoolSetting('kiosk.mode', T(32043, 'Start Plex On Kodi Startup'), False),
                OptionsSetting(
                    'kiosk.delay', T(33651, 'Start Delay'),
                    0,
                    [(0, T(32481))] + [
                        (a, T(33091).format(sec_or_ms=a, unit_s_or_ms="s")) for a in
                        list(range(1, 11, 1)) + list(range(15, 125, 5))]
                ),
                BoolSetting(
                    'auto_signin', T(32038, 'Automatically Sign In'), False
                ).description(
                    T(32100, 'Skip user selection and pin entry on startup.')
                ),
                BoolSetting(
                    'search_use_kodi_kbd', T(32955, 'Use Kodi keyboard for searching'), False
                ),
                ThemeMusicSetting('theme_music', T(32480, 'Theme music'), 5),
                PlayedThresholdSetting('played_threshold', T(33501, 'Video played threshold'), 1).description(
                    T(
                        33502,
                        "Set this to the same value as your Plex server (Settings>Library>Video played threshold) to av"
                        "oid certain pitfalls, Default: 90 %"
                    )
                )
            )
        ),
        'video': (
            T(32053, 'Video'), (
                QualitySetting('local_quality', T(32020, 'Local Quality'), 13),
                QualitySetting('remote_quality', T(32021, 'Remote Quality'), 13),
                QualitySetting('online_quality', T(32022, 'Online Quality'), 13),
                MultiOptionsSetting(
                    'playback_features', T(33058, ''),
                    ["playback_directplay", "playback_remux", "allow_4k"],
                    (
                        ('playback_directplay', T(32025, '')),
                        ('playback_remux', T(32026, '')),
                        ('allow_4k', T(32036, '')),
                    )
                ).description(T(33060, "").format(
                    feature_ds=T(32026, ''),
                    desc_ds=T(32979, ''),
                    feature_4k=T(32036, ''),
                    desc_4k=T(32102, ''))),
                MultiOptionsSetting(
                    'allowed_codecs', T(33059, ''),
                    ["allow_hevc", "allow_vc1"],
                    [
                        ('allow_hevc', T(32037, '')),
                        ('allow_vc1', T(32977, '')),
                    ] + ([('allow_av1', T(32601, ''))] if util.KODI_VERSION_MAJOR >= 20 else [])
                ).description(T(33061, "")),
            )
        ),
        'audio': (
            T(32931, 'Audio/Subtitles'), (
                BoolSetting('audio_clamp_to_orig', T(32058, 'Never exceed original audio codec'), True).description(
                    T(32059, 'When transcoding audio, never exceed the original audio bitrate or channel '
                             'count on the same codec.')
                ),
                BoolSetting('audio_channels_kodi', T(32060, 'Use Kodi audio channels'),
                            False).description(
                    T(32061, 'When transcoding audio, target the audio channels set in Kodi.')
                ),
                OptionsSetting(
                    'audio_force_ac3_cond',
                    T(32062, 'Transcode audio to AC3'),
                    'never',
                    (
                        ('never', T(32033, 'Never')),
                        ('always', T(32028, 'Always')),
                        ('2', '>2.0'),
                        ('3', '>2.1'),
                        ('5', '>5.0'),
                        ('6', '>5.1'),
                    )
                ).description(
                    T(32063, 'Transcode audio to AC3 in certain conditions (useful for passthrough).')
                ),
                BoolSetting('audio_ac3dts', T(32064, 'Treat DTS like AC3'),
                            True).description(
                    T(32065, 'When force AC3 settings are enabled, treat DTS the same as AC3 '
                             '(useful for Optical passthrough)')
                ),
                BoolSetting('audio_hires', T(33079, ''),
                            True).description(
                    T(33080, '')
                ),
                OptionsSetting(
                    'burn_subtitles',
                    T(32031, 'Burn-in Subtitles'),
                    'auto',
                    (('auto', T(32030, 'Auto')), ('image', T(32029, 'Only Image Formats')),
                     ('always', T(32028, 'Always')))
                ),
                BoolSetting('burn_ssa', T(32944, 'Burn-in SSA subtitles'),
                            True).description(
                    T(32945, 'When Direct Streaming instruct the Plex Server to burn in SSA/ASS subtitles (thus '
                             'transcoding the video stream). If disabled it will not touch the video stream, but '
                             'will convert the subtitle to unstyled text.')
                ),
                BoolSetting('forced_subtitles_override', T(32941, 'Forced subtitles fix'),
                            False).description(
                    T(32493, 'When a media file has a forced/foreign subtitle for a subtitle-enabled language, the Plex'
                             ' Media Server preselects it. This behaviour is usually not necessary and not configurable'
                             '. This setting fixes that by ignoring the PMSs decision and selecting the same language '
                             'without a forced flag if possible.')
                ),
                BoolSetting('calculate_oshash', T(32958, 'Calculate OpenSubtitles.com hash'),
                            False).description(
                    T(32959, '')
                ),
            )
        ),
        'ui': (
            T(32467, 'User Interface'), (
                OptionsSetting(
                    'theme',
                    T(32983, 'Theme'),
                    util.DEF_THEME,
                    (
                        ('modern', T(32985, 'Modern')),
                        ('modern-dotted', T(32986, 'Modern (dotted)')),
                        ('modern-colored', T(32989, 'Modern (colored)')),
                        ('classic', T(32987, 'Classic')),
                        ('custom', T(32988, 'Custom')),
                    ), theme_relevant=True
                ).description(
                    T(32984, 'stub')
                ),
                OptionsSetting(
                    'watched_indicators', T(33022, ''),
                    "modern_2024",
                    (
                        ('classic', T(32987, 'Classic')),
                        ('modern', T(32985, 'Modern')),
                        ('modern_2024', T(33076, 'Modern (2024)')),
                    ),
                    theme_relevant=True
                ).description(
                    T(33023, "")
                ),
                BoolSetting(
                    'hide_aw_bg', T(33024, ''), False, theme_relevant=True
                ).description(
                    T(33025, "")
                ),
                BoolSetting(
                    'scale_indicators', T(33077, ''), True, theme_relevant=True
                ).description(
                    T(33078, "")
                ),
                MultiOptionsSetting(
                    'no_episode_spoilers3', T(33006, ''),
                    ['unwatched'],
                    (
                        ('unwatched', T(33010, '')),
                        ('in_progress', T(33011, '')),
                        ('no_unwatched_episode_titles', T(33012, '')),
                        ('blur_chapters', T(33081, '')),
                    )
                ).description(T(33007, "")),
                MultiOptionsSetting(
                    'spoilers_allowed_genres2', T(33016, ''),
                    ["Reality", "Game Show", "Documentary", "Sport"],
                    [(g, g) for g in genres.GENRES_TV]
                ).description(T(33017, "")),
                BoolSetting(
                    'hubs_use_new_continue_watching', T(32998, ''), False
                ).description(
                    T(32999, "")
                ),
                BoolSetting(
                    'hubs_round_robin', T(33043, ''), False
                ).description(
                    T(33044, "").format(util.addonSettings.hubsRrMax)
                ),
                BoolSetting(
                    'hubs_bifurcation_lines', T(32961, 'Show hub bifurcation lines'), False
                ).description(
                    T(32962, "Visually separate hubs horizontally using a thin line.")
                ),
                BoolSetting(
                    'path_mapping_indicators', T(33032, 'Show path mapping indicators'), True
                ).description(
                    T(33033, "When path mapping is active for a library, display an indicator.")
                ),
                KeySetting('map_button_home', T(33085), None).description(T(33087))
            )
        ),
        'player': (
            T(32940, 'Player UI'), (
                BoolSetting('player_official', T(33045, 'Behave like official Plex clients'), True).description(
                    T(33046, '')),
                BoolSetting('no_osd_time_spoilers', T(33004, ''), False, backport_from="no_spoilers").description(
                    T(33005, '')),
                MultiUAOptionsSetting(
                    'player_show_buttons', T(33057, 'Show buttons'),
                    ['subtitle_downloads', 'skip_intro', 'skip_credits'],
                    (
                        ('subtitle_downloads', T(32932, 'Show subtitle quick-actions button')),
                        ('video_show_ffwdrwd', T(32933, 'Show FFWD/RWD buttons')),
                        ('video_show_repeat', T(32934, 'Show repeat button')),
                        ('video_show_shuffle', T(32935, 'Show shuffle button')),
                        ('skip_intro', T(32495, 'Skip Intro')),
                        ('skip_credits', T(32496, 'Skip Credits')),
                    )
                ).description(T(32939, 'Only applies to video player UI')),
                OptionsSetting(
                    'video_show_playlist', T(32936, 'Show playlist button'), 'eponly',
                    (
                        ('always', T(32035, 'Always')), ('eponly', T(32938, 'Only for Episodes/Playlists')),
                        ('never', T(32033, 'Never'))
                    )
                ).description(T(33088, 'Only applies to video player UI')),
                OptionsSetting(
                    'video_show_prevnext', T(32937, 'Show prev/next button'), 'eponly',
                    (
                        ('always', T(32035, 'Always')), ('eponly', T(32938, 'Only for Episodes/Playlists')),
                        ('never', T(32033, 'Never'))
                    )
                ).description(T(33088, 'Only applies to video player UI')),
                OptionsSetting(
                    'resume_seek_behind', T(33089, ''), 0,
                    [(0, T(32481))] + [
                        (a, T(33091).format(sec_or_ms=a if a < 1000 else int(a / 1000),
                                            unit_s_or_ms="ms" if a < 1000 else "s")) for a in
                        [100] + list(range(250, 1000, 250)) + list(range(1000, 61000, 1000))]
                ).description(T(33090, '')),
                BoolSetting('resume_seek_behind_pause', T(33092, ''), False).description(
                    T(33095, '')),
                OptionsSetting(
                    'resume_seek_behind_after', T(33093, ''), 0,
                    [(0, T(32481))] + [
                        (a, T(33091).format(sec_or_ms=a if a < 1000 else int(a / 1000),
                                            unit_s_or_ms="ms" if a < 1000 else "s")) for a in
                        list(range(250, 1000, 250)) + list(range(1000, 61000, 1000))]
                ).description(T(33094, '')),
                BoolSetting('resume_seek_behind_onlydp', T(33096, ''), True).description(
                    T(33097, '')),
                OptionsSetting(
                    'player_stop_on_idle',
                    T(32946, 'Stop video playback on idle after'),
                    0,
                    ((0, T(32033, 'Never')), (30, '30s'), (60, '1m'), (120, '2m'), (300, '5m'), (600, '10m'),
                     (900, '15m'), (1200, '20m'), (1800, '30m'), (2700, '45m'), (3600, '1h'),)
                ),
                BoolSetting(
                    'player_stop_on_screensaver', T(32947, 'Stop video playback on screensaver'), False
                ),
            )
        ),
        'player_user': (
            T(32631, 'Playback (user-specific)'), (
                BoolUserSetting(
                    'show_chapters', T(33601, 'Show video chapters'), True
                ).description(
                    T(33602, 'If available, show video chapters from the video-file instead of the '
                             'timeline-big-seek-steps.')
                ),
                BoolUserSetting(
                    'virtual_chapters', T(33603, 'Use virtual chapters'), True
                ).description(
                    T(33604, 'When the above is enabled and no video chapters are available, simulate them by using the'
                             ' markers identified by the Plex Server (Intro, Credits).')
                ),
                BoolUserSetting(
                    'auto_skip_in_transcode', T(32948, 'Allow auto-skip when transcoding'), True
                ).description(
                    T(32949, 'When transcoding/DirectStreaming, allow auto-skip functionality.')
                ),
                BoolUserSetting(
                    'post_play_auto', T(32039, 'Post Play Auto Play'), True
                ).description(
                    T(
                        32101,
                        "If enabled, when playback ends and there is a 'Next Up' item available, it will be automatical"
                        "ly be played after a {} second delay."
                    ).format(util.addonSettings.postplayTimeout)
                ),
                BoolUserSetting(
                    'post_play_never', T(33652, 'Never show Post Play'), False
                ),
                BoolUserSetting(
                    'binge_mode', T(33618, 'TV binge-viewing mode'), False
                ).description(
                    T(33619, 'Automatically skips episode intros, credits and tries to skip episode recaps. Doesn\'t '
                             'skip the intro of the first episode of a season and doesn\'t skip the final credits of a '
                             'show.\n\nCan be disabled/enabled per TV show.'
                             '\n\nOverrides any playback setting below.')
                ),
                BoolUserSetting(
                    'auto_skip_intro', T(32522, 'Automatically Skip Intro'), False
                ).description(
                    T(32523, 'Automatically skip intros if available. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.')
                ),
                BoolUserSetting(
                    'auto_skip_credits', T(32526, 'Auto Skip Credits'), False
                ).description(
                    T(32527, 'Automatically skip credits if available. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.')
                ),
                BoolUserSetting(
                    'show_intro_skip_early', T(33505, 'Show intro skip button early'), False
                ).description(
                    T(33506, 'Show the intro skip button from the start of a video with an intro marker. The auto-skipp'
                             'ing setting applies. Doesn\'t override enabled binge mode.\nCan be disabled/enabled per TV show.')
                ),
                BoolUserSetting(
                    'skip_post_play_tv', T(32973, 'Episodes: Skip Post Play screen'), False
                ).description(
                    T(32974, 'When finishing an episode, don\'t show Post Play but go to the next one immediately.'
                             '\nCan be disabled/enabled per TV show. Doesn\'t override enabled binge mode. '
                             'Overrides the Post Play setting.')
                ),
            )
        ),
        'network': (
            T(33624, 'Network'), (
                OptionsSetting(
                    'allow_insecure', T(32032), 'never',
                    (('never', T(32033)), ('same_network', T(32034)), ('always', T(32035)))
                ).description(
                    T(32104, 'When to connect to servers with no secure connections...')
                ),
                BoolSetting('smart_discover_local', T(33625, 'Smart LAN/local server discovery'), True)
                    .description(
                    T(33626, "Checks whether servers returned from Plex.tv are actually local/in your LAN. "
                             "For specific setups (e.g. Docker) Plex.tv might not properly detect a local "
                             "server.\n\nNOTE: Only works on Kodi 19 or above."
                      )
                ),
                BoolSetting('prefer_local', T(33627, 'Prefer LAN/local servers over security'), False)
                    .description(
                    T(33628, "Prioritizes local connections over secure ones. Needs the proper setting in \"Allow "
                             "Insecure Connections\" and the Plex Server's \"Secure connections\" at \"Preferred\". "
                             "Can be used to enforce manual servers."
                      )
                ),
                BoolSetting('gdm_discovery', T(32042, 'Server Discovery (GDM)'), False),
                OptionsSetting(
                    'handle_plexdirect', T(32990), 'ask',
                    (('ask', T(32991)), ('always', T(32035)), ('never', T(32033)))
                ).description(
                    T(32992, 'stub')
                ),
                IPSetting('manual_ip_0', T(32044, 'Connection 1 IP'), ''),
                IntegerSetting('manual_port_0', T(32045, 'Connection 1 Port'), 32400),
                IPSetting('manual_ip_1', T(32046, 'Connection 2 IP'), ''),
                IntegerSetting('manual_port_1', T(32047, 'Connection 2 Port'), 32400),
            )
        ),
        'system': (
            T(33600, 'System'), (

                BoolSetting('exit_default_is_quit', T(32965, 'Start Plex On Kodi Startup'), False)
                .description(T(32966, "stub")),
                BoolSetting('path_mapping', T(33000, ''), True).description(T(33001, '')),
                BufferSetting('cache_size',
                              T(33613, 'Kodi Buffer Size (MB)'),
                              20,
                              [(mem, '{} MB'.format(mem)) for mem in lib.cache.kcm.viableOptions])
                .description(
                    '{}{}'.format(T(33614, 'stub1').format(
                        lib.cache.kcm.free, lib.cache.kcm.recMax),
                        '' if lib.cache.kcm.useModernAPI else ' ' + T(32954, 'stub2'))
                ) if not util.FROM_KODI_REPOSITORY or lib.cache.kcm.useModernAPI else None,
                ReadFactorSetting('readfactor',
                                  T(32922, 'Kodi Cache Readfactor'),
                                  4,
                                  [(rf, str(rf) if rf > 0 else T(32976, 'stub')) for rf in lib.cache.kcm.readFactorOpts])
                .description(
                    T(32923, 'Sets the Kodi cache readfactor value. Default: {0}, recommended: {1}.'
                             'With "Slow connection" enabled this will be set to {2}, as otherwise the cache doesn\'t'
                             'fill fast/aggressively enough.').format(lib.cache.kcm.defRF,
                                                                      lib.cache.kcm.recRFRange,
                                                                      lib.cache.kcm.defRFSM)
                ) if not util.FROM_KODI_REPOSITORY or lib.cache.kcm.useModernAPI else None,
                BoolSetting(
                    'slow_connection', T(32915, 'Slow connection'), False
                ).description(T(32916, "Use with a wonky/slow connection, "
                                       "e.g. in a hotel room. Adjusts the UI to visually "
                                       "wait for item refreshes and waits for the buffer to fill when starting "
                                       "playback.")),
                OptionsSetting(
                    'action_on_sleep',
                    T(32700, 'Action on Sleep event'),
                    'none',
                    (('none', T(32702, 'Nothing')), ('stop', T(32703, 'Stop playback')),
                     ('quit', T(32704, 'Quit Kodi')), ('reboot', T(32426, 'Reboot')),
                     ('shutdown', T(32423, 'Shutdown')),
                     ('hibernate', T(32425, 'Hibernate')), ('suspend', T(32424, 'Suspend')),
                     ('cecstandby', T(32705, 'CEC Standby')), ('logoff', T(32421, 'Sign Out')))
                ).description(T(32701, 'When Kodi receives a sleep event from the system, run the following action.')),
                OptionsSetting(
                    'action_on_wake',
                    T(33070, 'Action on Wake event'),
                    util.isCoreELEC and 'wait_5' or 'wait_1',
                    [('none', T(32702, 'Nothing')), ('restart', T(33071, 'Restart PM4K'))]
                    + [('wait_{}'.format(s), T(33072, '').format(s)) for s in [1, 2, 3] + list(range(5, 65, 5))]
                ).description(T(33075, '')),
                BoolSetting('debug', T(32024, 'Debug Logging'), False),
                BoolSetting('dump_config', T(33642, 'Debug Logging'), False).description(T(33643)),
            )
        ),
        'privacy': (
            T(32051, 'Privacy'),
            ()
        ),
        'about': (
            T(32052, 'About'), (
                InfoSetting('addon_version', T(32054, 'Addon Version'), util.ADDON.getAddonInfo('version')),
                InfoSetting('kodi_version', T(32055, 'Kodi Version'), xbmc.getInfoLabel('System.BuildVersion')),
                PlatformSetting(),
                InfoSetting('screen_res', T(32056, 'Screen Resolution'),
                            xbmc.getInfoLabel('System.ScreenResolution').split('-')[0].strip()),
                ServerVersionSetting('server_version', T(32057, 'Current Server Version'), None),
                InfoSetting('addon_path', T(33616, 'Addon Path'), util.ADDON.getAddonInfo("path")),
                InfoSetting('userdata_path', T(33617, 'Userdata/Profile Path'),
                            util.translatePath("special://profile")),
            )
        ),
    }

    SECTION_IDS = ('main', 'video', 'audio', 'ui', 'player', 'player_user', 'network', 'system', 'about')

    def __getitem__(self, key):
        return self.SETTINGS[key]


class SettingsWindow(kodigui.BaseWindow, windowutils.UtilMixin):
    xmlFile = 'script-plex-settings.xml'
    path = util.ADDON.getAddonInfo('path')
    theme = 'Main'
    res = '1080i'
    width = 1920
    height = 1080

    SECTION_LIST_ID = 75
    SETTINGS_LIST_ID = 100
    OPTIONS_LIST_ID = 125
    TOP_GROUP_ID = 200

    CLOSE_BUTTON_ID = 201
    PLAYER_STATUS_BUTTON_ID = 204

    def onFirstInit(self):
        self.settings = Settings()
        self.sectionList = kodigui.ManagedControlList(self, self.SECTION_LIST_ID, 6)
        self.settingsList = kodigui.ManagedControlList(self, self.SETTINGS_LIST_ID, 6)
        self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 6)

        self.setProperty('heading', T(32343, 'Settings'))
        self.showSections()
        self.setFocusId(75)
        self.lastSection = None
        self.checkSection()

    def onAction(self, action):
        try:
            self.checkSection()
            controlID = self.getFocusId()
            if action in (xbmcgui.ACTION_STOP, xbmcgui.ACTION_CONTEXT_MENU):
                self.editSetting(clear=True)
                return
            elif action in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU):
                if controlID == self.OPTIONS_LIST_ID:
                    self.setFocusId(self.SETTINGS_LIST_ID)
                    return
                elif controlID == self.SETTINGS_LIST_ID:
                    self.setFocusId(self.SECTION_LIST_ID)
                    return
                # elif not xbmc.getCondVisibility('ControlGroup({0}).HasFocus(0)'.format(self.TOP_GROUP_ID)):
                #     self.setFocusId(self.TOP_GROUP_ID)
                #     return
            elif action == xbmcgui.ACTION_MOVE_RIGHT and controlID == 150:
                self.editSetting(from_right=True)
        except:
            util.ERROR()

        kodigui.BaseWindow.onAction(self, action)

    def onClick(self, controlID):
        if controlID == self.SECTION_LIST_ID:
            self.setFocusId(self.SETTINGS_LIST_ID)
        elif controlID == self.SETTINGS_LIST_ID:
            self.editSetting()
        elif controlID == self.OPTIONS_LIST_ID:
            self.changeSetting()
        elif controlID == self.CLOSE_BUTTON_ID:
            self.doClose()
        elif controlID == self.PLAYER_STATUS_BUTTON_ID:
            self.showAudioPlayer()

    def checkSection(self):
        mli = self.sectionList.getSelectedItem()
        if not mli:
            return

        if mli.dataSource == self.lastSection:
            return

        self.lastSection = mli.dataSource
        self.showSettings(self.lastSection)
        self.setProperty('section.about', self.lastSection == 'about' and '1' or '')
        util.DEBUG_LOG('Settings: Changed section ({0})', self.lastSection)

    def showSections(self):
        items = []
        for sectionID in self.settings.SECTION_IDS:
            label = self.settings[sectionID][0]
            item = kodigui.ManagedListItem(label, data_source=sectionID)
            items.append(item)
        items[-1].setProperty('is.last', '1')

        self.sectionList.addItems(items)

    def showSettings(self, section):
        settings = self.settings[section][1]
        if not settings:
            return self.settingsList.reset()

        items = []
        for setting in settings:
            if setting is None:
                continue

            item = kodigui.ManagedListItem(setting.label, setting.type != 'BOOL' and setting.valueLabel() or '',
                                           data_source=setting)
            item.setProperty('description', setting.desc)
            if setting.type == 'BOOL':
                item.setProperty('checkbox', '1')
                item.setProperty('checkbox.checked', setting.get() and '1' or '')
            elif setting.type == 'BUTTON':
                item.setProperty('button', '1')

            if setting.userAware:
                item.setProperty('useraware', '1')

            items.append(item)

        self.settingsList.reset()
        self.settingsList.addItems(items)

    def editSetting(self, from_right=False, clear=False):
        mli = self.settingsList.getSelectedItem()
        if not mli:
            return

        setting = mli.dataSource

        if clear and setting.type != 'STRING':
            return

        if setting.type in ('LIST', 'OPTIONS', 'MULTI'):
            self.fillList(setting)
        elif setting.type == 'BOOL' and not from_right:
            self.toggleBool(mli, setting)
        elif setting.type == 'IP' and not from_right:
            self.editIP(mli, setting)
        elif setting.type == 'INTEGER' and not from_right:
            self.editInteger(mli, setting)
        elif setting.type == 'STRING' and not from_right:
            self.editString(mli, setting, clear=clear)
        elif setting.type == 'BUTTON':
            self.buttonDialog(mli, setting)

    def changeSetting(self):
        optionItem = self.optionsList.getSelectedItem()
        if not optionItem:
            return

        mli = self.settingsList.getSelectedItem()
        if not mli:
            return

        setting = mli.dataSource

        if setting.type == 'LIST':
            setting.set(optionItem.pos())
            mli.setLabel2(setting.valueLabel())
        elif setting.type == 'OPTIONS':
            setting.set(optionItem.dataSource)
            mli.setLabel2(setting.valueLabel())
        elif setting.type == 'MULTI':
            values = setting.get()
            if optionItem.dataSource in values:
                values.remove(optionItem.dataSource)
                optionItem.setProperty('checkbox.checked', '')
            else:
                values.append(optionItem.dataSource)
                optionItem.setProperty('checkbox.checked', '1')
            setting.set(values)
            mli.setLabel2(setting.valueLabel(values=values))

        if setting.type != 'MULTI':
            self.setFocusId(self.SETTINGS_LIST_ID)

    def fillList(self, setting):
        mli = self.settingsList.getSelectedItem()
        if not mli:
            return

        items = []
        if setting.type == 'LIST':
            for label in setting.optionLabels():
                items.append(kodigui.ManagedListItem(label))
        elif setting.type in ('OPTIONS', 'MULTI'):
            for ID, label in setting.options:
                items.append(kodigui.ManagedListItem(label, data_source=ID))

        self.optionsList.reset()
        self.optionsList.addItems(items)
        idx = setting.optionIndex()
        if isinstance(idx, int):
            idx = [idx]
        for _idx in idx:
            if setting.type == 'MULTI':
                self.optionsList[_idx].setProperty('checkbox.checked', '1')
        if idx:
            self.optionsList.selectItem(idx[-1])
        self.setFocusId(self.OPTIONS_LIST_ID)

    def toggleBool(self, mli, setting):
        setting.set(not setting.get())
        mli.setProperty('checkbox.checked', setting.get() and '1' or '')

    def editIP(self, mli, setting):
        current = setting.get()
        edit = True
        if current:
            edit = xbmcgui.Dialog().yesno(
                T(32412, 'Edit Or Clear'),
                T(32413, 'Edit IP address or clear the current setting?'),
                nolabel=T(32414, 'Clear'),
                yeslabel=T(32415, 'Edit')
            )

        if edit:
            result = xbmcgui.Dialog().input(T(32416, 'Enter IP Address'), current, xbmcgui.INPUT_IPADDRESS)
            if not result:
                return
        else:
            result = ''

        setting.set(result)
        mli.setLabel2(result)

    def editInteger(self, mli, setting):
        result = xbmcgui.Dialog().input(T(32417, 'Enter Port Number'), str(setting.get()), xbmcgui.INPUT_NUMERIC)
        if not result:
            return
        setting.set(int(result))
        mli.setLabel2(result)

    def editString(self, mli, setting, clear=False):
        if clear:
            setting.set(None)
            mli.setLabel2(T(32447, "None"))
            return
        if hasattr(setting, "value_setter"):
            result = setting.value_setter()
            if result is not None:
                setting.set(result.code)
                mli.setLabel2(str(result))
            return
        else:
            result = xbmcgui.Dialog().input(T(32417, 'Enter Port Number'), str(setting.get()), xbmcgui.INPUT_STRING)
        if result is None:
            return
        elif result == -1:
            setting.set(None)
            return

        setting.set(result)
        mli.setLabel2(str(result))


class SchnorchelDialog(xbmcgui.WindowXMLDialog):
    """
    inspired by https://github.com/pkscout/script.keymap/blob/main/editor.py
    """

    def __new__(cls, *args, **kwargs):
        gui_api = tuple(map(int, xbmcaddon.Addon('xbmc.gui').getAddonInfo('version').split('.')))
        file_name = "DialogNotification.xml" if gui_api >= (5, 11, 0) else "DialogKaiToast.xml"
        return super(SchnorchelDialog, cls).__new__(cls, file_name, "")

    def __init__(self, timeout=10):
        self.key = None
        self.timeout = timeout
        self._winID = None

        self.setProperty("no.image", "1")

    def onInit(self):
        ctrl1, ctrl2 = 401, 402
        if util.SKIN_PLEXTUARY:
            ctrl1, ctrl2 = 1401, 1402
        try:
            self.getControl(ctrl1).addLabel(T(33085))
            self.getControl(ctrl2).addLabel(T(33086).format(self.timeout))
        except AttributeError:
            self.getControl(ctrl1).setLabel(T(33085))
            self.getControl(ctrl2).setLabel(T(33086).format(self.timeout))

    def setProperty(self, key, value):
        if not self._winID:
            self._winID = xbmcgui.getCurrentWindowId()

        try:
            xbmcgui.Window(self._winID).setProperty(key, value)
            xbmcgui.WindowXML.setProperty(self, key, value)
        except RuntimeError:
            xbmc.log('kodigui.BaseWindow.setProperty: Missing window', xbmc.LOGDEBUG)

    def onAction(self, action):
        code = action.getButtonCode()
        action_id = action.getId()
        self.key = None
        if action_id not in (xbmcgui.ACTION_SELECT_ITEM, xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU):
            self.key = None if code == 0 else actions.ActionKey(code)
        elif action_id in (xbmcgui.ACTION_NAV_BACK, xbmcgui.ACTION_PREVIOUS_MENU):
            self.key = None
        else:
            self.key = -1
        self.close()
        return


class SelectDialog(kodigui.BaseDialog, util.CronReceiver):
    xmlFile = 'script-plex-settings_select_dialog.xml'
    path = util.ADDON.getAddonInfo('path')
    theme = 'Main'
    res = '1080i'
    width = 1920
    height = 1080

    OPTIONS_LIST_ID = 100

    def __init__(self, *args, **kwargs):
        kodigui.BaseDialog.__init__(self, *args, **kwargs)
        self.heading = kwargs.get('heading')
        self.options = kwargs.get('options')
        self.choice = None

    def onFirstInit(self):
        self.optionsList = kodigui.ManagedControlList(self, self.OPTIONS_LIST_ID, 8)
        self.setProperty('heading', self.heading)
        self.showOptions()
        util.CRON.registerReceiver(self)

    def onAction(self, action):
        try:
            if not xbmc.getCondVisibility('Player.HasMedia'):
                self.doClose()
                return
        except:
            util.ERROR()

        kodigui.BaseDialog.onAction(self, action)

    def onClick(self, controlID):
        if controlID == self.OPTIONS_LIST_ID:
            self.setChoice()

    def onClosed(self):
        util.CRON.cancelReceiver(self)

    def tick(self):
        if not xbmc.getCondVisibility('Player.HasMedia'):
            self.doClose()
            return

    def setChoice(self):
        mli = self.optionsList.getSelectedItem()
        if not mli:
            return

        self.choice = self.options[self.optionsList.getSelectedPosition()][0]
        self.doClose()

    def showOptions(self):
        items = []
        for o in self.options:
            item = kodigui.ManagedListItem(o[1], data_source=o[0])
            items.append(item)

        self.optionsList.reset()
        self.optionsList.addItems(items)

        self.setFocusId(self.OPTIONS_LIST_ID)


def showOptionsDialog(heading, options):
    w = SelectDialog.open(heading=heading, options=options)
    choice = w.choice
    del w
    return choice


def showAudioDialog(video):
    options = [(s, s.getTitle()) for s in video.audioStreams]
    choice = showOptionsDialog(T(32048, 'Audio'), options)
    if choice is None:
        return

    video.selectStream(choice)


def showSubtitlesDialog(video):
    options = [(s, s.getTitle()) for s in video.subtitleStreams]
    options.insert(0, (plexnet.plexstream.NoneStream(), 'None'))
    choice = showOptionsDialog(T(32396, 'Subtitles'), options)
    if choice is None:
        return

    video.selectStream(choice)


def showQualityDialog(video):
    options = [(13 - i, T(l)) for (i, l) in enumerate((32001, 32002, 32003, 32004, 32005, 32006, 32007, 32008, 32009,
                                                       32010, 32011))]

    choice = showOptionsDialog(T(32397, 'Quality'), options)
    if choice is None:
        return

    video.settings.setPrefOverride('local_quality', choice)
    video.settings.setPrefOverride('remote_quality', choice)
    video.settings.setPrefOverride('online_quality', choice)


def openWindow():
    w = SettingsWindow.open()
    del w
