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

import json
import sys
import datetime
import types

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

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__"

#MAIN_LANGUAGES = [l for l in languages if l.part1]
#SEP_LANGUAGES= [l for l in languages if l.part2t and l not in MAIN_LANGUAGES]

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

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

    def should_show(self):
        return self.show_cb() if self.show_cb else True

    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, show_cb=None):
        self.ID = ID
        self.label = label
        self.default = default
        self.desc = desc
        self.isThemeRelevant = theme_relevant
        self.backport_from = backport_from
        if show_cb:
            self.show_cb = show_cb
        util.DEFAULT_SETTINGS[ID] = default

    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(32017),
        T(32002),
        T(32016),
        T(32003),
        T(32004),
        T(32005),
        T(32015),
        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)

        # fixme: not sure why we initialize default with an empty list in the supercall
        util.DEFAULT_SETTINGS[ID] = default

    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 UAListSetting(ListSetting, UserAwareSetting):
    def __init__(self, ID, label, default, options, **kwargs):
        super(UAListSetting, self).__init__(ID, label, default, **kwargs)
        self.options = options

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


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):
        if isinstance(self.info, types.FunctionType):
            return self.info()
        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(
                    'ensure_lastused', T(34062, 'Ensure Kodi Addon position'), True
                ).description(
                    T(34063, 'When not run from the Program addons section, our lastused value will not be updated. Make sure it does when the addon exits.')
                ),
                BoolSetting(
                    'search_use_kodi_kbd', T(32955, 'Use Kodi keyboard for searching'), False
                ),
                ThemeMusicSetting('theme_music', T(32480, 'Theme music'), 5),
                BoolSetting(
                    'theme_music_loop', T(33737, 'Loop theme music'), False
                ),
                PlayedThresholdSetting('played_threshold', T(33501, 'Video played threshold'), 1,
                                       show_cb=lambda: plexnet.plexapp.SERVERMANAGER.selectedServer.prefs.get("LibraryVideoPlayedThreshold", None) is None
                                       ).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 %"
                    )
                ),
                OptionsSetting(
                    'played_threshold_behaviour',
                    T(34022, 'Video play completion behaviour'),
                    3,
                    (
                        (0, T(34024, 'at selected threshold percentage')),
                        (1, T(34025, 'at final credits marker position')),
                        (2, T(34026, 'at first credits marker position')),
                        (3, T(34027, 'earliest between threshold percent and first credits marker')),
                    ),
                    show_cb=lambda: plexnet.plexapp.SERVERMANAGER.selectedServer.prefs.get(
                        "LibraryVideoPlayedAtBehaviour", None) is None
                ).description(T(34023, "Decide whether to use end credits markers to determine the 'watched' "
                                       "state of video items. When markers are not available the selected threshold "
                                       "percentage will be used.")),
                BoolSetting('use_alternate_seek2', T(33667, 'Use alternate seek'), util.altSeekRecommended).description(
                    T(33668, 'ATTENTION: Only enable this if you have reproducible audio issues after '
                             'seeking/resuming.\n\nUse an alternative seek method in videos, which can help in '
                             'problematic scenarios; brings its own issues/quirks. Disabled by default (enabled by '
                             'default for CoreELEC and LG WebOS)'
                )),
                BoolSetting('lav_mode_auto_switch', T(34073, 'Seamless branching fix'), True)
                .description(T(34074, "Automatically temporarily activate LAV Full or SB mode in CoreELEC on "
                                      "supported builds to alleviate audio dropouts on certain titles, if necessary. "
                                      "Based on \"seamless_branching.json\", can be user-amended by putting a file "
                                      "with the same name and structure into userdata/addon_data/script.plexmod."))
                if util.CE_SB_LAV_SWITCH else None,
                BoolSetting(
                    'assume_resume', T(33711, 'Always resume media'), True
                ).description(
                    T(33712, 'When playback of an in-progress media is requested, resume it by default instead'
                             ' of asking whether to resume or start from the beginning.')
                ),
            )
        ),
        'video': (
            T(32053, 'Video'), (
                QualitySetting('local_quality2', T(32020, 'Local Quality'), 16),
                QualitySetting('remote_quality2', T(32021, 'Remote Quality'), 16),
                QualitySetting('online_quality2', T(32022, 'Online Quality'), 16),
                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, ''))),
                BoolSetting(
                    'disable_hdr', T(33660, 'Disable HDR'), False
                ).description(T(33661, "If you don't want your client to handle HDR (or HDR-fallback), "
                                       "enable this to force transcoding. Doesn't apply to DV Profile 5.")
                ),
                BoolSetting(
                    'clamp_video_bitrates', T(33685, 'Clamp video bitrate'), True
                ).description(T(33686, "Only show bitrate targets lower than the current video's bitrate.")
                ),
                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)')
                ),
                MultiOptionsSetting(
                    'audio_disabled_codecs', T(33665, 'Disable audio codecs'),
                    [],
                    list(sorted([(a, "{} ({})".format(b, a)) for a, b in plexnet.util.AUDIO_CODECS_VERB.items()]))
                ).description(
                    T(33666, "Audio codecs you can't play back. Disables Direct Play for such media items, "
                             "enables Direct Stream if possible, transcodes audio stream to compatible format.")
                ),
                OptionsSetting(
                    'audio_transcode_codec', T(33733, 'Transcode target codec'),
                    "default",
                    [("default", T(32030, 'Auto'))] + list(sorted([(a, "{} ({})".format(b, a)) for a, b in plexnet.util.AUDIO_CODECS_TC_VERB.items()]))
                ).description(
                    T(33734, "Sets the target codec when transcoding/direct streaming. Overridden when "
                             "\"Transcode audio to AC3\" is set.")
                ),
                BoolSetting('audio_hires', T(33079, ''),
                            True).description(
                    T(33080, '')
                ),
                MultiOptionsSetting(
                    'disable_subtitle_languages', T(33691, "Native languages"),
                    [],
                    [(b, a) for a, b in sorted([(l.name, l.part2t) for l in languages if l.part1])]  # +
                    # [(b, a) for a, b in sorted([(l.name, l.part2t) for l in SEP_LANGUAGES])]
                ).description(
                    T(33692, "When you usually watch things in a different language with subtitles, but are a"
                             " native speaker of other languages, which you don't need subtitles for, prevent Plex "
                             "from auto-selecting subtitles for those languages. Also used to match external audio "
                             "files when that setting is enabled. Your Plex \"Preferred Audio Language\" is added to "
                             "this list automatically while your Plex subtitle auto-select mode is \"Shown with "
                             "foreign audio\"; forced subtitles are always kept.")
                ),
                BoolSetting('use_external_audio', T(34098, 'Honor external audio files'), False)
                .description(T(34099, 'When external audio files are found alongside the video '
                                      '(e.g. a separate .dts or .ac3 file), use them. A single external '
                                      'audio file is always used. When multiple are present, the '
                                      'Plex-selected audio language is preferred, then native languages. '
                                      'Selectable in preplay and playback audio settings. '
                                      'Only for mapped Direct Play.')),
                OptionsSetting(
                    'subtitle_download_from',
                    T(33693, 'Download subtitles using'),
                    'plex',
                    (
                        ('ask', T(33694, 'Ask')),
                        ('plex', 'Plex'),
                        ('kodi', 'Kodi'),
                    )
                ).description(T(33695, "Where do you want to download subtitles from? Note: Currently this "
                                       "only applies to the subtitle quick actions in the player. The subtitle download"
                                       " in stream settings always uses Plex as a source.")),
                BoolSetting('subtitle_download_fallback', T(33701, 'Fallback to Kodi'),
                            True).description(
                    T(33702, "When no subtitles are found via the Plex subtitle search, fall back to Kodi "
                             "subtitle search. Note: Currently this only applies to the subtitle quick actions in the "
                             "player. The subtitle download in stream settings always uses Plex as a source.")
                ),
                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.')
                ),
                BoolUserSetting('auto_sync', T(33655, 'Auto-Sync Subtitles'),
                            True).description(
                    T(33656, 'Only for External SRT subtitles. The PMS setting for voice activity detection '
                             'has to be enabled for this to work.')
                ),
                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. This is skipped when the audio is one of your Native '
                             'languages, so forced subtitles are kept in that case.')
                ),
                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",
                    (
                        ('none', T(32309, 'None')),
                        ('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, "")
                ),
                OptionsSetting(
                    'hub_count',
                    T(34087, 'Hub Count'),
                    8,
                    ((6, '6'), (8, '8'), (10, '10'), (12, '12'), (16, '16')),
                    theme_relevant=True
                ).description(
                    T(34088, 'Number of hub rows displayed on the home screen. More rows require more memory.')
                ),
                BoolSetting(
                    'clear_logos', T(35046, 'Show title logos on detail screens'), True
                ).description(
                    T(35047, "Replace the written title with the logo image your server has stored for the "
                             "item. Items without a logo keep their written title.")
                ),
                BoolUserSetting(
                    'use_watchlist', T(34007, 'Use Watchlist'), True
                ).description(
                    T(34008, "Activates the current user's Plex watchlist as a section item. Adds watchlist "
                             "functionality to certain media screens. Per-user setting. Default: On")
                ),
                BoolUserSetting(
                    'watchlist_auto_remove', T(34009, 'Watchlist auto-remove'), True
                ).description(
                    T(34010, "Automatically remove fully watched items from watchlist. Default: On")
                ),
                MultiOptionsSetting(
                    'show_ratings', T(33709, 'Show ratings for'),
                    ["series", "movies"],
                    [
                        ('series', T(32393, 'TV Shows')),
                        ('movies', T(32348, 'Movies')),
                    ]
                ),
                MultiOptionsSetting(
                    'show_reviews1', T(33710, 'Show reviews for'),
                    ["watched", "unwatched"],
                    [
                        ('watched', T(33718, 'Watched')),
                        ('unwatched', T(33010, 'Unwatched')),
                    ]
                ),
                BoolUserSetting(
                    'show_directors', T(34061, 'Show directors in cast lists'), True
                ),
                MultiOptionsSetting(
                    'no_episode_spoilers4', T(33006, ''),
                    ['unwatched', 'blur_images', 'hide_summary'],
                    (
                        ('unwatched', T(33010, '')),
                        ('in_progress', T(33011, '')),
                        ('no_unwatched_episode_titles', T(33012, '')),
                        ('blur_images', T(33706, '')),
                        ('blur_resume_images', T(33707, '')),
                        ('blur_chapters', T(33081, '')),
                        ('hide_summary', T(33708, '')),
                        ('hide_ratings', T(33705, '')),
                    )
                ).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(
                    'home_confirm_actions', T(33663, 'Home: Confirm item actions'), True
                ).description(
                    T(33664, "When acting on items in the Home view, such as mark played, hide from continue "
                             "watching etc., show a confirmation dialog.")
                ),
                BoolSetting(
                    'disable_exit_on_back', T(35007, 'Home: Disable exit on Back'), False
                ).description(
                    T(35008, "When enabled, pressing Back on the Home screen no longer shows the "
                             "exit prompt and will not exit Plex. You can still exit from the Exit "
                             "option in your username menu.")
                ),
                BoolSetting(
                    'hub_season_thumbnails', T(33740, 'Home: Episodes season thumbnails'), True
                ).description(
                    T(33741, "Use season thumbnails/posters when displaying episodes in hubs instead of "
                             "the TV show's.")
                ),
                BoolSetting(
                    'home_inprogress_resume', T(33713, 'Home: Resume in-progress items'), False
                ).description(
                    T(33714, 'Resume in-progress items directly instead of visiting the media.')
                ),
                BoolSetting(
                    'hubs_use_new_continue_watching', T(32998, 'Use new Continue Watching hub on Home'), False
                ).description(
                    T(32999, "Instead of separating Continue Watching and On Deck hubs, behave like the modern "
                             "Plex clients, which combine those two types of hubs into one Continue Watching hub.")
                ),
                BoolSetting(
                    'hubs_round_robin', T(33043, ''), False
                ).description(
                    T(33044, "").format(util.addonSettings.hubsRrMax)
                ),
                BoolSetting(
                    'hubs_linear', T(34091, 'Linear Hubs'), False
                ).description(
                    T(34092, "Certain hubs, such as Top Unwatched Movies, are randomized by Plex by default, "
                             "making sequentially consuming content practically impossible. Prevent this and "
                             "use linearly sorted variants.")
                ),
                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, '')),
                BoolUserSetting('preplay_preroll', T(34051, 'Movies: Play pre-rolls'), False).description(
                    T(34052, 'Plays pre-roll clips (server-defined) before playing a movie without a resume '
                             'point. User-specific.')),
                BoolUserSetting('preplay_preroll_first', T(34054, 'Play pre-rolls before trailers'), True).description(
                    T(34055, 'Official Plex clients play the trailers first, then the preroll clips. Enabling '
                             'this will do the opposite. Default: On. User-specific.')),
                UAListSetting(
                    'preplay_trailers', T(34053, 'Cinema Trailers to Play Before Movies'),
                    5,
                    list(map(str, list(range(6))))
                ),
                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'] + (['video_show_vs10'] if util.CE_VS10 else []),
                    (
                        ('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')),
                    ) + (
                        (('video_show_vs10', T(34090, 'VS10 Mode')),) if util.CE_VS10 else ()
                    )
                ).description(T(32939, 'Only applies to video player UI')),
                MultiUAOptionsSetting(
                    'fast_pause_resume', T(34012, 'Fast pause/resume'),
                    [],
                    (
                        ('paused', T(34013, 'when paused')),
                        ('playing', T(34014, 'when playing')),
                    )
                ).description(T(34015, 'User-specific. Use OK/ENTER button to pause instead of showing the OSD'
                                       ' (which can then only be accessed using DOWN), or resume when paused. '
                                       'Only works with \'Behave like official Plex clients\' enabled.')),
                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_offset', T(34075, "Resume offset adjustment"), -3500,
                    [(0, T(32481))] + [
                        (a, T(33091).format(sec_or_ms=a if a > -1000 else "{:.1f}".format(a / 1000),
                                            unit_s_or_ms="ms" if a > -1000 else "s")) for a in
                        [-100] + list(range(-250, -1000, -250)) + list(range(-1000, -30000, -500))]
                ).description(T(34076, 'When resuming media with a resume point, negatively adjust it by this '
                                       'amount of milliseconds to avoid lost content due to keyframes/seek behaviour. '
                                       'Default: -3.5s')),
                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, '')),
                BoolSetting('seek_back_on_start', T(34049, 'Seek back on start'), util.altSeekRecommended).description(
                    T(34050, "Issue a quick seek forward then back to the start of the video, when we start "
                             "fresh (no resume point, no marker to immediately skip). Can fix A/V desync issues (e.g. "
                             "CoreELEC on Ugoos with passthrough) and embedded subtitles showing late, with certain "
                             "setups. Only for DirectPlay. This still requires a sensible value for \"Delay after "
                             "change of refresh rate\" in Kodi (default: Off, CE/LG: On)")),
                BoolSetting('initial_seek_blackout', T(34060, 'Blackout screen during initial seek'), True),
                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(
                    'combined_chapters', T(35001, 'Combine video & virtual chapters'), True
                ).description(
                    T(35002, 'Combine video & virtual chapters when both are available.')
                ),
                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: Continuous playback'), 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.')
                ),
                OptionsSetting(
                    'tv_specials_order',
                    T(34103, 'Specials in episode queue'),
                    'default',
                    (
                        ('default', T(34105, 'Library default')),
                        ('interleave', T(34107, 'Interleave by air date')),
                    )
                ).description(
                    T(34104, "Library default keeps Plex's order in the queue, with specials at the "
                             "front - playback skips past them and starts at the first regular episode, "
                             "so they appear in the queue but won't play. "
                             "Interleave inserts each special by its original air date and plays them "
                             "between regular episodes.")
                ),
                IntegerSetting(
                    'shuffle_catchup_series_count', T(35014, 'Shuffle Catchup: number of series'), 5
                ).description(
                    T(35015, 'How many series to queue in Catchup shuffle mode.')
                ),
                IntegerSetting(
                    'shuffle_catchup_episode_threshold',
                    T(35016, 'Shuffle Catchup: max unwatched episodes per series'), 3
                ).description(
                    T(35017, 'Only include a series in Catchup mode when it has this many or fewer '
                             'unwatched episodes left.')
                ),
            )
        ),
        'network': (
            T(33624, 'Network'), (
                BoolSetting('local_mode', T(35021, 'Local mode (no plex.tv)'), False)
                    .description(
                    T(35029, "Only talk to local Plex Media Servers and never contact plex.tv. "
                             "Best enabled via \"Go local\" in the user menu, which prepares local user "
                             "switching while plex.tv is still reachable. Watchlist, Discover and other "
                             "plex.tv-based features are disabled. Takes full effect after a restart of "
                             "the addon.")
                ),
                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')
                ),
                BoolSetting(
                    'force_pd_mapping', T(34038, 'Force plex.direct mapping'), False
                ).description(T(34039, '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('auto_update_check', T(33672, 'Check for updates'), True)
                .description(T(33673, "Automatically check for updates periodically. If installed from a "
                                      "Kodi repository and the Update Source setting is set to Repository, Kodi "
                                      "itself will handle the updating of this addon. "
                                      "Needs a Kodi restart when changed.")) if not util.FROM_KODI_REPOSITORY else None,
                BoolSetting('update_check_startup', T(33674, 'Check for updates on start'), True)
                .description(T(33675, "Automatically check for updates on startup. "
                                      "Doesn't do much when Update source is Repository."
                                      "Needs a Kodi restart when changed.")) if not util.FROM_KODI_REPOSITORY else None,
                OptionsSetting(
                    'update_source',
                    T(33676, 'Update source'),
                    'repository',
                    (('beta', T(33678, 'Beta')), ('stable', T(33679, 'Stable')),
                     ('repository', T(33680, 'Repository')))
                ).description(T(33677, 'Specifies the update mode. Will immediately check for a new version '
                                       'when changed and closing settings.\nDefault: Repository\n\nBeta: Bleeding '
                                       'edge (possibly unstable)\nStable: Stable branch (faster than Repository)\n'
                                       'Repository: Kodi repository (official (slow) or Don\'t Panic)')
                              ) if not util.FROM_KODI_REPOSITORY else None,
                MultiOptionsSetting(
                    'cache_requests', T(33724, 'Cache Plex data for'),
                    [],
                    [
                        ('items', T(33723, 'Media Items')),
                        ('libraries', T(33722, 'Libraries')),
                    ]
                ).description(T(33727, "Store Plex server responses for items and library views in a local "
                                       "SQLite database. Doesn't cache anything else (Home/Hubs are always up to date)."
                                       " Massively speeds up consecutive visits to items and libraries. Certain "
                                       "important events, such as watch state changes, automatically delete the item "
                                       "cache and its corresponding library cache. The complete cache gets cleared "
                                       "when exiting the addon. (Default: Off)")),
                BoolSetting('persist_requests_cache', T(33725, 'Persist cached Plex data'), False)
                .description(T(33726, "Instead of clearing the cache when exiting the addon, persist it "
                                      "instead. Warning: You'll most likely encounter missing items in libraries "
                                      "or outdated data. Use the corresponding menu functionalities to clear the "
                                      "cache for specific items or libraries.")),
                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.")),
                BoolSetting(
                    'onss_library_back_home', T(34064, 'Return to Home on Screensaver'), util.CE_NEEDS_HOME_ON_SCREENSAVER
                ).description(T(34065, 'When Kodi starts its screensaver, and we are in a library view, '
                                       'return to home. This saves idle energy as the library views are very heavy '
                                       'on the CPU even on idle due to a Kodi bug.'),),
                OptionsSetting(
                    'action_on_sleep',
                    T(32700, 'Action on Sleep event'),
                    'none',
                    (('none', T(32702, 'Nothing')), ('stop', T(32703, 'Stop playback')),
                     ('home', T(34097, 'Go Home')),
                     ('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.altSeekRecommended 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")),
                InfoSetting('service_status', T(33689, 'Service running'),
                            lambda: "{} ({})".format(
                                util.getGlobalProperty("service.started") and T(32328, "Yes") or T(32329, "No"),
                                util.getGlobalProperty("service.version"))),
                InfoSetting('i_last_update_check', T(33690, "Last update check"),
                            lambda: util.getGlobalProperty('last_update_check', datetime.datetime.fromtimestamp(0).strftime('%Y-%m-%dT%H:%M:%S.%f'))),
            )
        ),
    }

    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.lastFocusID = 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:
                if self.lastFocusID == self.SECTION_LIST_ID:
                    if self.lastSection != 'about':
                        self.setFocusId(self.SETTINGS_LIST_ID)
                    return
                elif self.lastFocusID == self.SETTINGS_LIST_ID:
                    self.editSetting(from_right=True)
                    return
        except:
            util.ERROR()

        kodigui.BaseWindow.onAction(self, action)

    def onClick(self, controlID):
        if controlID == self.SECTION_LIST_ID:
            if self.lastSection != 'about':
                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 onFocus(self, controlID):
        self.lastFocusID = controlID

    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 or not setting.should_show():
                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)
        except TypeError:
            # python 2.7
            pass

    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 = [(16 - i, T(l)) for (i, l) in enumerate((32001, 32017, 32002, 32016, 32003, 32004, 32005, 32015, 32006,
                                                       32007, 32008, 32009, 32010, 32011))]

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

    video.settings.setPrefOverride('local_quality2', choice)
    video.settings.setPrefOverride('remote_quality2', choice)
    video.settings.setPrefOverride('online_quality2', choice)


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