# -*- coding: utf-8 -*-
"""Home widgets of skin.milionar: defaults and the widget manager (docs/plan/milionar-library/070 §2, §5).

A widget slot is a set of skin strings `mil.<sec>.<n>.type|limit|sort|style|title` (+ `mil.<sec>.hidewatched|upcoming`).
The generated include `MilionarSlots_<sec>` (scripts/skin_harness/gen_slots.py) turns them into Estuary widgets; by
the manager offers all of them (max 7). This module only writes strings
(`Skin.SetString`), so the plugin never touches a control. Imported through the main.py fast path.
"""
import json
import copy
import uuid

import xbmc
import xbmcaddon
import xbmcgui

from wcs.library2 import clicks
from wcs.skin.hero_contract import SECTIONS as HERO_SECTIONS, WIDGET_SLOTS

ADDON_ID = 'plugin.video.milionar'
SLOTS = WIDGET_SLOTS
LIMITS = ('10', '20', '30', '50')

# section -> (addon string id of its name, [row ids a slot may show], default slots [(row, limit, sort, style)])
SECTIONS = {
    'vinyl': (32500, ['vinyl_curator', 'vinyl_discover', 'vinyl_songs', 'vinyl_top_tracks', 'vinyl_albums', 'vinyl_artists',
                      'vinyl_playlists', 'vinyl_genres', 'vinyl_radios', 'vinyl_favorite_songs',
                      'vinyl_favorite_playlists', 'vinyl_favorite_albums', 'vinyl_favorite_artists', 'home_vinyl_actions'],
              [('vinyl_curator', '20', 'source', 'banner'), ('vinyl_genres', '20', 'source', 'categories'),
               ('vinyl_discover', '20', 'source', 'square'), ('vinyl_playlists', '20', 'source', 'square'),
               ('vinyl_favorite_songs', '20', 'source', 'record'), ('vinyl_artists', '20', 'source', 'artist'),
               ('home_vinyl_actions', '20', '', 'categories')]),
    'foryou': (35300, ['fy_continue', 'fy_for_you', 'fy_top10', 'fy_because_1', 'fy_because_2', 'fy_because_3',
                       'fy_tonight', 'fy_new', 'fy_gems'],
               [('fy_continue', '20', '', 'episode'), ('fy_for_you', '20', '', 'poster'),
                ('fy_top10', '10', '', 'poster'), ('fy_because_1', '20', '', 'poster'),
                ('fy_tonight', '20', '', 'landscape'), ('fy_new', '20', '', 'poster')]),
    'cont': (32350, ['cont_movies', 'cont_series', 'recent_played', 'widget_mytv_next', 'widget_mytv_channels'],
             [('widget_mytv_channels', '20', '', 'poster'), ('cont_series', '20', 'activity', 'episode'),
              ('cont_movies', '20', 'activity', 'landscape')]),
    'my_movies': (32351, ['my_movies_progress', 'my_movies_added', 'my_movies_unwatched', 'my_movies_genres',
                          'my_movies_watched', 'my_movies_all', 'home_movies_actions'],
                  [('my_movies_genres', '20', '', 'categories'), ('my_movies_progress', '20', 'activity', 'landscape'),
                   ('my_movies_added', '20', 'added', 'poster'), ('home_movies_actions', '20', '', 'categories')]),
    'my_series': (32352, ['my_series_next', 'my_series_new', 'my_series_added', 'my_series_unwatched', 'my_series_genres',
                          'my_series_upcoming', 'my_series_all', 'home_series_actions'],
                  [('my_series_genres', '20', '', 'categories'), ('my_series_next', '20', 'activity', 'episode'),
                   ('my_series_added', '10', 'added', 'poster'), ('home_series_actions', '20', '', 'categories')]),
    'mytv': (32353, ['widget_mytv_channels', 'widget_mytv_next'],
             [('widget_mytv_channels', '20', '', 'poster'), ('widget_mytv_next', '20', '', 'episode')]),
    'mycinema': (32354, ['widget_mycinema_channels', 'widget_mycinema_recommended'],
                 [('widget_mycinema_channels', '20', '', 'poster'), ('widget_mycinema_recommended', '20', '', 'poster')]),
    'game': (32355, ['widget_demo_movies', 'widget_demo_series', 'widget_game_topics'],
             [('widget_demo_movies', '20', '', 'poster'), ('widget_demo_series', '20', '', 'landscape'),
              ('widget_game_topics', '20', '', 'poster')]),
    'ai': (32356, ['widget_ai_recent'], [('widget_ai_recent', '20', '', 'poster')]),
    'kraska': (32357, ['widget_kraska_root'], [('widget_kraska_root', '20', '', 'categories')]),
    'tmdb': (32358, ['widget_tmdb_popular_movies', 'widget_tmdb_popular_series', 'widget_csfd_charts'],
             [('widget_tmdb_popular_movies', '20', '', 'poster'), ('widget_tmdb_popular_series', '20', '', 'poster'),
              ('widget_csfd_charts', '20', '', 'categories')]),
}
HIDE_WATCHED = ('my_movies', 'my_series')          # behaviour switches per section
UPCOMING = ('cont', 'my_series')

# row id -> addon string id of its title
TITLES = {
    'home_movies_actions': 32755, 'home_series_actions': 32755, 'home_vinyl_actions': 32755,
    'vinyl_curator': 32501, 'vinyl_discover': 32502, 'vinyl_songs': 32503, 'vinyl_top_tracks': 32690, 'vinyl_albums': 32504,
    'vinyl_artists': 32505, 'vinyl_playlists': 32506, 'vinyl_genres': 32507, 'vinyl_radios': 32508,
    'vinyl_favorite_songs': 32510, 'vinyl_favorite_playlists': 32511,
    'vinyl_favorite_albums': 32512, 'vinyl_favorite_artists': 32513,
    'cont_movies': 32300, 'cont_series': 32301, 'recent_played': 32302, 'my_movies_progress': 32300,
    'my_movies_added': 32303, 'my_movies_unwatched': 32304, 'my_movies_genres': 32305, 'my_movies_watched': 32306,
    'my_movies_all': 32307, 'my_series_next': 32308, 'my_series_new': 32309, 'my_series_added': 32310,
    'my_series_unwatched': 32311, 'my_series_genres': 32312, 'my_series_upcoming': 32313, 'my_series_all': 32314,
    'widget_mytv_channels': 32315, 'widget_mytv_next': 32316, 'widget_mycinema_channels': 32317,
    'widget_mycinema_recommended': 32318, 'widget_game_topics': 32319, 'widget_ai_recent': 32320,
    'widget_demo_movies': 32740, 'widget_demo_series': 32741,
    'widget_kraska_root': 32321, 'widget_tmdb_popular_movies': 32322, 'widget_tmdb_popular_series': 32323,
    'widget_csfd_charts': 32324,
    'fy_continue': 35301, 'fy_for_you': 35300, 'fy_top10': 35303, 'fy_because_1': 35321, 'fy_because_2': 35322,
    'fy_because_3': 35323, 'fy_tonight': 35324, 'fy_new': 35310, 'fy_gems': 35311,
}
# rows whose sort and style the user may choose (the catalogue rows of rows.py); the others are fixed
SORTS = {'cont_movies': ('activity', 'title', 'year', 'rating', 'random'),
         'cont_series': ('activity', 'title', 'rating', 'random'),
         'recent_played': (), 'my_movies_genres': (), 'my_series_genres': ()}
MOVIE_SORTS = ('activity', 'added', 'title', 'year', 'rating', 'random')
SERIES_SORTS = ('activity', 'added', 'title', 'year', 'rating', 'random')
SORT_TITLES = {'activity': 32330, 'added': 32331, 'title': 32332, 'year': 32333, 'rating': 32334, 'random': 32335,
               'time': 32336, 'air': 32337, 'source': 32527}
STYLE_TITLES = {'poster': 32340, 'landscape': 32341, 'episode': 32342, 'categories': 32343, 'square': 32534,
                'record': 32685, 'artist': 32686, 'banner': 32687, 'track': 32688}
OFF_TITLE, EMPTY_ROW_TITLE = 32344, 32345


def _text(string_id):
    return xbmcaddon.Addon(ADDON_ID).getLocalizedString(string_id)


def _set(name, value):
    """Skin string (an empty value removes it). Synchronous, so a read right after a write sees the new value."""
    value = value.replace('"', "'")
    if xbmc.getInfoLabel('Skin.String({})'.format(name)) == value:
        return False
    command = 'Skin.SetString({},"{}")'.format(name, value) if value != '' else 'Skin.Reset({})'.format(name)
    xbmc.executebuiltin(command, True)
    return True


def sorts_of(row):
    if row.startswith('vinyl_'):
        return ('source', 'title', 'random')
    if row in SORTS:
        return SORTS[row]
    if row.startswith('my_movies'):
        return MOVIE_SORTS
    if row.startswith('my_series') and row not in ('my_series_new', 'my_series_upcoming'):
        return SERIES_SORTS
    return ()


EPISODE_ROWS = ('cont_series', 'my_series_next', 'my_series_new', 'my_series_upcoming')
POSTER_ROWS = ('cont_movies', 'recent_played', 'my_movies_progress', 'my_movies_added', 'my_movies_unwatched',
               'my_movies_watched', 'my_movies_all', 'my_series_added', 'my_series_unwatched', 'my_series_all')


def styles_of(row):
    """Styles a slot may use for `row`: the catalogue rows of rows.py have a choice, the old rows are fixed."""
    if row.startswith('home_'):
        return ('categories',)
    if row == 'vinyl_genres':
        return ('categories', 'landscape', 'banner')
    if row in ('vinyl_artists', 'vinyl_favorite_artists'):
        return ('artist', 'square', 'landscape', 'banner')
    if row in ('vinyl_songs', 'vinyl_favorite_songs', 'vinyl_top_tracks'):
        return ('square', 'record', 'track', 'landscape')
    if row.startswith('vinyl_'):
        return ('square', 'landscape', 'banner', 'record')
    if row.endswith('_genres'):
        return ('categories',)
    if row == 'fy_continue':
        return ('episode',)
    if row.startswith('fy_'):
        return ('poster', 'landscape')
    if row in EPISODE_ROWS:
        return ('episode',)
    if row in POSTER_ROWS:
        return ('poster', 'landscape')
    if row == 'widget_demo_movies':
        return ('poster', 'landscape')
    if row == 'widget_demo_series':
        return ('landscape', 'poster')
    for _sec, (_n, _rows, defaults) in SECTIONS.items():
        for default in defaults:
            if default[0] == row:
                return (default[3],)
    return ('poster',)


def default_for(row, sec):
    for default in SECTIONS[sec][2]:
        if default[0] == row:
            return default
    return (row, '20', (sorts_of(row) or ('',))[0], styles_of(row)[0])


KEEP = object()                          # write_slot: leave the stored hero buttons as they are


def write_slot(sec, n, row, limit, sort, style, click=None, uid='', menu=None, hero=KEEP):
    prefix = 'mil.{}.{}.'.format(sec, n)
    from wcs.skin import context
    values = {'type': row, 'limit': limit if row != 'off' else '', 'sort': sort if row != 'off' else '',
              'style': style if row != 'off' else '',
              'click': (clicks.resolve(row, click) if click is not None else clicks.default(row)) if row != 'off' else '',
              'title': _text(TITLES[row]) if row in TITLES else '',
              'uid': (uid or xbmc.getInfoLabel('Skin.String({}uid)'.format(prefix)) or uuid.uuid4().hex) if row != 'off' else '',
              'menu': context.encode(menu) if row != 'off' else ''}
    if row == 'off' or hero is not KEEP:
        values['hero'] = context.encode(hero) if row != 'off' else ''
    return {key for key, value in values.items() if _set(prefix + key, value)}


def touch_hero(sec):
    """New hero button revision of the section: it is part of every row URL there (gen_slots.CONTENT), because the
    buttons are written to the items, also to those of the row a genre row borrows its hero from."""
    if sec in HERO_SECTIONS:
        _set('mil.{}.herorev'.format(sec), uuid.uuid4().hex[:8])


def _refresh_slots(sec, numbers):
    """Config changes belong to placements, not shared data revisions of a row family."""
    if numbers:
        home = xbmcgui.Window(10000)
        token = 'config:' + uuid.uuid4().hex
        for n in numbers:
            home.setProperty('milionar.widgets.{}.{}'.format(sec, n), token)


def _write_section(sec, widgets, hw, up):
    affected, layout_changed, buttons_changed = set(), False, False
    for n in range(1, SLOTS + 1):
        if n <= len(widgets):
            w = widgets[n - 1]
            fields = write_slot(sec, n, w['type'], w['limit'], w['sort'], w['style'],
                                w.get('click'), w.get('uid', ''), w.get('menu'), w.get('hero'))
        else:
            fields = write_slot(sec, n, 'off', '', '', '')
        if fields:
            affected.add(n)
            layout_changed |= bool(fields & {'type', 'style'})
            buttons_changed |= bool(fields & {'type', 'hero'})
    flags_changed = _set('mil.{}.hidewatched'.format(sec), '1' if hw else '')
    flags_changed |= _set('mil.{}.upcoming'.format(sec), '1' if up else '')
    if flags_changed:
        affected.update(range(1, len(widgets) + 1))
    if buttons_changed:
        touch_hero(sec)
    _refresh_slots(sec, affected)
    return layout_changed


def write_defaults(sec):
    from wcs.skin import defaults
    shipped = defaults.section(sec)
    if shipped is not None:                                 # the shipped snapshot of resources/defaults/skin.json
        return _write_section(sec, shipped['widgets'], shipped['hidewatched'], shipped['upcoming'])
    widgets = [dict(zip(('type', 'limit', 'sort', 'style'), values)) for values in SECTIONS[sec][2]]
    return _write_section(sec, widgets, False, sec in UPCOMING)


def reload_skin():
    """Only type/style/layout changes need their include conditions reparsed."""
    xbmc.executebuiltin('ReloadSkin()', True)


def apply_defaults(only=None):
    """Write the default slots (all sections, or one) and remember that the defaults were applied once."""
    layout_changed = False
    for sec in ([only] if only else SECTIONS):
        layout_changed |= write_defaults(sec)
    if not only:
        xbmc.executebuiltin('Skin.SetBool(MilionarSlotsV1)')
    return layout_changed


# --------------------------------------------------------------------------- the widget manager (070 §5)
# Window 1123: the widgets of one section as a list (left) and buttons (right). Every change goes into a draft kept in
# Window(Home).Property(milionar.wm); "Uložit" writes changed strings and reloads only for changed layout.
DRAFT = 'milionar.wm'
MENU_SECTIONS = {'mil_foryou': 'foryou', 'mil_vinyl': 'vinyl', 'mil_continue': 'cont', 'mil_mymovies': 'my_movies', 'mil_myseries': 'my_series', 'mil_mytv': 'mytv',
                 'mil_mycinema': 'mycinema', 'mil_game': 'game', 'mil_ai': 'ai', 'mil_kraska': 'kraska', 'mil_tmdb': 'tmdb'}
S_ADD, S_TYPE, S_COUNT, S_SORT, S_STYLE, S_DONE, S_DISCARD, S_ALL_USED, S_CHANGED = \
    32364, 32365, 32366, 32367, 32368, 32369, 32370, 32371, 32374


def current(sec, n):
    prefix = 'mil.{}.{}.'.format(sec, n)
    from wcs.skin import context
    result = {key: xbmc.getInfoLabel('Skin.String({}{})'.format(prefix, key))
              for key in ('type', 'limit', 'sort', 'style', 'click', 'uid')}
    result['menu'] = context.decode(xbmc.getInfoLabel('Skin.String({}menu)'.format(prefix)))
    result['hero'] = context.decode(xbmc.getInfoLabel('Skin.String({}hero)'.format(prefix)))
    return result


def _widget(row, sec):
    default = default_for(row, sec)
    return {'type': default[0], 'limit': default[1], 'sort': default[2], 'style': default[3], 'click': clicks.default(row),
            'uid': uuid.uuid4().hex, 'menu': None, 'hero': None}


def has_count(row):
    """Only the catalogue rows of rows.py honour the item count; the old rows (MyTV, TMDb ...) ignore it."""
    return row in TITLES and not row.startswith(('widget_', 'home_'))


def info(widget):
    """Right-hand text of a list row: count · sort · style."""
    parts = []
    if has_count(widget['type']):
        parts.append(widget['limit'])
    if widget['sort'] in SORT_TITLES and len(sorts_of(widget['type'])) > 1:
        parts.append(_text(SORT_TITLES[widget['sort']]))
    if widget['style'] in STYLE_TITLES and len(styles_of(widget['type'])) > 1:
        parts.append(_text(STYLE_TITLES[widget['style']]))
    if len(clicks.options(widget['type'])) > 1:
        parts.append(clicks.label(widget['type'], clicks.resolve(widget['type'], widget.get('click'))))
    return ' · '.join(parts)


def _draft():
    raw = xbmcgui.Window(10000).getProperty(DRAFT)
    try:
        return json.loads(raw) if raw else None
    except ValueError:
        return None


def _snapshot(draft):
    return {'widgets': copy.deepcopy(draft['widgets']), 'hw': draft['hw'], 'up': draft['up']}


def _dirty(draft):
    return _snapshot(draft) != draft['orig']


def _store(draft):
    """Keep the draft and copy what the window shows into skin strings (`mil.edit.*`)."""
    xbmcgui.Window(10000).setProperty(DRAFT, json.dumps(draft))
    sec, widgets = draft['sec'], draft['widgets']
    _set('mil.edit.header', _text(SECTIONS[sec][0]))
    _set('mil.edit.hero', '1' if sec in HERO_SECTIONS else '')
    _set('mil.edit.dirty', _text(S_CHANGED) if _dirty(draft) else '')
    _set('mil.edit.count', '{} / {}'.format(len(widgets), SLOTS))
    for n in range(1, SLOTS + 1):
        if n <= len(widgets):
            _set('mil.edit.{}'.format(n), _text(TITLES[widgets[n - 1]['type']]))
            _set('mil.edit.{}.info'.format(n), info(widgets[n - 1]))
        else:
            _set('mil.edit.{}'.format(n), '')
            _set('mil.edit.{}.info'.format(n), '')
    _set('mil.edit.add', _text(S_ADD) if len(widgets) < SLOTS else '')
    on, off = _text(32346), _text(32347)
    _set('mil.edit.hw', '{}: {}'.format(_text(32348), on if draft['hw'] else off) if sec in HIDE_WATCHED else '')
    _set('mil.edit.up', '{}: {}'.format(_text(32349), on if draft['up'] else off) if sec in UPCOMING else '')


def open_manager(sec):
    """Build the draft from the current skin strings and open window 1123."""
    widgets = []
    for n in range(1, SLOTS + 1):
        slot = current(sec, n)
        if slot['type'] and slot['type'] != 'off' and slot['type'] in TITLES:
            slot['click'] = clicks.resolve(slot['type'], slot['click'])
            slot['uid'] = slot['uid'] or '{}:{}'.format(sec, n)
            widgets.append(slot)
    draft = {'sec': sec, 'widgets': widgets,
             'hw': bool(xbmc.getInfoLabel('Skin.String(mil.{}.hidewatched)'.format(sec))),
             'up': bool(xbmc.getInfoLabel('Skin.String(mil.{}.upcoming)'.format(sec)))}
    draft['orig'] = _snapshot(draft)
    _store(draft)
    xbmc.executebuiltin('ActivateWindow(1123)')


def _selected():
    """0-based index of the focused list row (the row after the last widget is "+ Přidat widget")."""
    current_item = xbmc.getInfoLabel('Container(9000).CurrentItem')
    return int(current_item) - 1 if current_item.isdigit() else 0


def _move_cursor(offset):
    if offset:
        xbmc.executebuiltin('Control.Move(9000,{})'.format(offset))


def _choose(heading, labels, current=-1):
    return xbmcgui.Dialog().select(heading, labels, preselect=current)


def _pick_row(sec, widgets, keep=None):
    """Select one of the rows a section may show (not those already used); the row id or None."""
    used = {w['type'] for w in widgets if w['type'] != keep}
    rows_ = [r for r in SECTIONS[sec][1] if r not in used and not r.startswith('vinyl_favorite_')]
    favorites = [r for r in SECTIONS[sec][1] if r.startswith('vinyl_favorite_') and r not in used]
    if favorites:
        rows_.append('vinyl_favorites')
    if not rows_:
        xbmcgui.Dialog().notification('Milionář', _text(S_ALL_USED), xbmcgui.NOTIFICATION_INFO)
        return None
    pick = _choose(_text(32360), [_text(32509 if r == 'vinyl_favorites' else TITLES[r]) for r in rows_],
                   rows_.index(keep) if keep in rows_ else -1)
    if pick < 0:
        return None
    if rows_[pick] == 'vinyl_favorites':
        chosen = _choose(_text(32528), [_text(TITLES[r]) for r in favorites],
                         favorites.index(keep) if keep in favorites else -1)
        return favorites[chosen] if chosen >= 0 else None
    return rows_[pick]


def edit_widget(draft, index):
    """One window with the values of a widget (content, count, sort, style); a click changes that value only."""
    sec, widgets = draft['sec'], draft['widgets']
    widget = widgets[index]
    pick = -1
    while True:
        entries = [('type', _text(S_TYPE).format(_text(TITLES[widget['type']])))]
        if widget['type'].startswith('vinyl_favorite_'):
            entries.append(('favorite', _text(32528) + ': ' + _text(TITLES[widget['type']])))
        if has_count(widget['type']):
            entries.append(('limit', _text(S_COUNT).format(widget['limit'])))
        if len(sorts_of(widget['type'])) > 1:
            entries.append(('sort', _text(S_SORT).format(_text(SORT_TITLES[widget['sort']]) if widget['sort'] in SORT_TITLES else '')))
        if len(styles_of(widget['type'])) > 1:
            entries.append(('style', _text(S_STYLE).format(_text(STYLE_TITLES[widget['style']]))))
        if len(clicks.options(widget['type'], sec)) > 1:
            entries.append(('click', _text(clicks.S_CLICK).format(clicks.label(widget['type'], widget['click']))))
        entries.append(('context', _text(32600)))
        entries.append(('done', _text(S_DONE)))
        pick = _choose(_text(TITLES[widget['type']]), [label for _key, label in entries], max(pick, 0))   # stay on the row
        if pick < 0 or entries[pick][0] == 'done':
            return
        key = entries[pick][0]
        if key == 'context':
            edit_context(draft, index)
        elif key == 'type':
            row = _pick_row(sec, widgets, keep=widget['type'])
            if row and row != widget['type']:
                widget.update(_widget(row, sec))                      # a new content starts from its own defaults
        elif key == 'favorite':
            used = {w['type'] for j, w in enumerate(widgets) if j != index}
            options = [r for r in SECTIONS[sec][1] if r.startswith('vinyl_favorite_') and r not in used]
            chosen = _choose(_text(32528), [_text(TITLES[r]) for r in options], options.index(widget['type']))
            if chosen >= 0 and options[chosen] != widget['type']:
                count, sort, style = widget['limit'], widget['sort'], widget['style']
                widget.update(_widget(options[chosen], sec))
                widget.update(limit=count, sort=sort, style=style)
        elif key == 'limit':
            chosen = _choose(_text(32361), list(LIMITS), LIMITS.index(widget['limit']) if widget['limit'] in LIMITS else -1)
            if chosen >= 0:
                widget['limit'] = LIMITS[chosen]
        elif key == 'sort':
            options = sorts_of(widget['type'])
            chosen = _choose(_text(32362), [_text(SORT_TITLES[o]) for o in options],
                             options.index(widget['sort']) if widget['sort'] in options else -1)
            if chosen >= 0:
                widget['sort'] = options[chosen]
        elif key == 'click':
            options = clicks.options(widget['type'], sec)
            chosen = _choose(_text(clicks.S_CHOOSE), [clicks.label(widget['type'], o) for o in options],
                             options.index(widget['click']) if widget['click'] in options else -1)
            if chosen >= 0:
                widget['click'] = options[chosen]
        elif key == 'style':
            options = styles_of(widget['type'])
            chosen = _choose(_text(32363), [_text(STYLE_TITLES[o]) for o in options],
                             options.index(widget['style']) if widget['style'] in options else -1)
            if chosen >= 0:
                widget['style'] = options[chosen]
        _store(draft)


def commit(draft):
    """Write only changed values; refresh edited placements and reload only their layout."""
    sec = draft['sec']
    layout_changed = _write_section(sec, draft['widgets'], draft['hw'], draft['up'])
    xbmcgui.Window(10000).clearProperty(DRAFT)
    xbmc.executebuiltin('Dialog.Close(1123)')
    if layout_changed:
        reload_skin()


def wm_op(op):
    """A button of the window: up, down, edit, remove, add, defaults, hw, up_toggle, save, cancel."""
    draft = _draft()
    if not draft:
        return
    sec, widgets, i = draft['sec'], draft['widgets'], _selected()
    on_widget = i < len(widgets)
    if op == 'up' and on_widget and i > 0:
        widgets[i - 1], widgets[i] = widgets[i], widgets[i - 1]
        _store(draft)
        _move_cursor(-1)
    elif op == 'down' and on_widget and i < len(widgets) - 1:
        widgets[i + 1], widgets[i] = widgets[i], widgets[i + 1]
        _store(draft)
        _move_cursor(1)
    elif op == 'context' and on_widget:
        edit_context(draft, i)
    elif op == 'hero' and on_widget and sec in HERO_SECTIONS:
        edit_hero(draft, i)
    elif op == 'edit' and on_widget:
        edit_widget(draft, i)
    elif op in ('add', 'edit') and not on_widget and len(widgets) < SLOTS:
        row = _pick_row(sec, widgets)
        if row:
            widgets.append(_widget(row, sec))
            _store(draft)
    elif op == 'remove' and on_widget:
        widgets.pop(i)
        _store(draft)
        _move_cursor(-1 if i >= len(widgets) and i > 0 else 0)
    elif op == 'defaults':
        draft['widgets'] = [_widget(d[0], sec) for d in SECTIONS[sec][2]]
        draft['hw'], draft['up'] = False, sec in UPCOMING
        _store(draft)
    elif op == 'hw' and sec in HIDE_WATCHED:
        draft['hw'] = not draft['hw']
        _store(draft)
    elif op == 'up_toggle' and sec in UPCOMING:
        draft['up'] = not draft['up']
        _store(draft)
    elif op == 'save':
        if _dirty(draft):
            commit(draft)
        else:
            xbmcgui.Window(10000).clearProperty(DRAFT)
            xbmc.executebuiltin('Dialog.Close(1123)')
    elif op == 'cancel':
        if not _dirty(draft) or xbmcgui.Dialog().yesno('Milionář', _text(S_DISCARD)):
            xbmcgui.Window(10000).clearProperty(DRAFT)
            xbmc.executebuiltin('Dialog.Close(1123)')


def edit_context(draft, index):
    from wcs.skin.context_dialogs import edit
    widget = draft['widgets'][index]
    config = edit(widget['type'], widget.get('menu'))
    if config is not None:
        widget['menu'] = config
        _store(draft)


def edit_hero(draft, index):
    from wcs.skin.context_dialogs import edit_hero as edit
    widget = draft['widgets'][index]
    config = edit(widget['type'], widget.get('hero'))
    if config is not None:
        widget['hero'] = config
        _store(draft)


def edit_saved_hero(identity):
    """The hero button editor of one saved widget (the context menu, More options, C on a hero button)."""
    from wcs.skin import context
    from wcs.skin.context_dialogs import edit_hero as edit
    sec, n, widget = context.find_widget(identity)
    if widget and sec in HERO_SECTIONS:
        config = edit(widget['type'], widget.get('hero'))
        if config is not None:
            fields = write_slot(sec, n, widget['type'], widget['limit'], widget['sort'], widget['style'], widget['click'],
                                widget['uid'] or identity, widget.get('menu'), config)
            if fields:
                touch_hero(sec)
                _refresh_slots(sec, {n})


def edit_saved_context(identity):
    from wcs.skin import context
    from wcs.skin.context_dialogs import edit
    sec, n, widget = context.find_widget(identity)
    if widget:
        config = edit(widget['type'], widget.get('menu'), title=context.text(32704).format(context.text(TITLES[widget['type']])))
        if config is not None:
            fields = write_slot(sec, n, widget['type'], widget['limit'], widget['sort'], widget['style'], widget['click'],
                                widget['uid'] or identity, config)
            if fields:
                _refresh_slots(sec, {n})


def open_for_menu_item(menu_id):
    """The Info key on a Home main menu item opens the widget manager of that section (070 §5)."""
    sec = MENU_SECTIONS.get(menu_id)
    if sec:
        open_manager(sec)


def run(params):
    action, sec = params.get('action', ''), params.get('sec', '')
    if action == 'skin_defaults':                           # once per install, from the Home <onload> (wcs/skin/defaults.py)
        from wcs.skin import defaults
        if defaults.apply():
            reload_skin()
    elif action == 'skin_slots_defaults':
        if apply_defaults(sec or None):
            reload_skin()
    elif action == 'skin_slots' and sec in SECTIONS:
        open_manager(sec)
    elif action == 'skin_wm':
        wm_op(params.get('op', ''))
    elif action == 'skin_menu_item':
        open_for_menu_item(params.get('menu', ''))
    elif action == 'skin_context_edit':
        edit_saved_context(params.get('ctx', ''))
