#!/usr/bin/env python3
"""Make the settings of the running Kodi the shipped defaults.

Reads the addon and skin settings from the Kodi userdata and writes them as

  * the `default=` of every bool/select/number/slider setting in resources/settings.xml,
  * resources/defaults/skin.json (skin flags, strings and the Home widget / context menu slots).

Credentials (API keys, tokens, logins, passwords) and device specific values are never read: text and folder
settings are skipped entirely and wcs.defaults.is_protected() is checked on top. wcs/defaults.py (addon) and
wcs/skin/defaults.py (skin) apply the result once to every install that has not been reset to these defaults yet.

    python3 scripts/snapshot_defaults.py          # write
    python3 scripts/snapshot_defaults.py --check  # only report what would change
"""
import glob
import json
import os
import re
import sys
import xml.etree.ElementTree as ET
from urllib.parse import unquote

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from wcs.defaults import RESET_TYPES, resettable_defaults  # noqa: E402

USERDATA = os.path.expanduser('~/Library/Application Support/Kodi/userdata/addon_data')
ADDON_SETTINGS = os.path.join(USERDATA, 'plugin.video.milionar', 'settings.xml')
SKIN_SETTINGS = os.path.join(USERDATA, 'skin.milionar', 'settings.xml')
SETTINGS_XML = os.path.join(ROOT, 'resources', 'settings.xml')
SKIN_JSON = os.path.join(ROOT, 'resources', 'defaults', 'skin.json')
HOME_XML = os.path.join(ROOT, 'resources', 'skin.milionar', 'xml', 'Home.xml')

SLOT_FIELDS = ('type', 'limit', 'sort', 'style', 'click', 'uid')       # + 'menu' and 'hero' (encoded JSON): the arguments of slots.write_slot
SLOT_KEY = re.compile(r'^mil\.(?!st\.|edit\.)([a-z_]+)\.(\d+)\.')      # mil.<section>.<n>.<field>: one Home widget slot
SECTION_FLAG = re.compile(r'^mil\.[a-z_]+\.(hidewatched|upcoming)$')
SKIP_STRINGS = re.compile(r'^mil\.(st|edit)\.|\.(rev|herorev)$|fanart\.', re.I)       # runtime state: row caches, editor drafts, revisions


def read_settings(path):
    return {s.get('id'): (s.get('type'), s.text or '') for s in ET.parse(path).getroot().iter('setting')}


def escape(value):
    return value.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')


def addon_defaults():
    """{id: value} of the user's resettable settings, in the order of resources/settings.xml."""
    user = read_settings(ADDON_SETTINGS)
    return {sid: user[sid][1] for sid in resettable_defaults(SETTINGS_XML) if sid in user}


def write_addon_defaults(values, check):
    text = open(SETTINGS_XML, encoding='utf-8').read()
    changed = []

    def tag_for(match):
        tag = match.group(0)
        sid = re.search(r'\bid="([^"]*)"', tag)
        if not sid or sid.group(1) not in values or not re.search(r'\btype="(%s)"' % '|'.join(RESET_TYPES), tag):
            return tag
        new = escape(values[sid.group(1)])
        old = re.search(r'\bdefault="([^"]*)"', tag)
        if old and old.group(1) == new:
            return tag
        changed.append((sid.group(1), old.group(1) if old else None, new))
        if old:
            return tag[:old.start()] + 'default="{}"'.format(new) + tag[old.end():]
        return re.sub(r'\s*/?>$', lambda m: ' default="{}"{}'.format(new, m.group(0).lstrip()), tag)

    text = re.sub(r'<setting\b[^<>]*>', tag_for, text)
    if not check:
        open(SETTINGS_XML, 'w', encoding='utf-8').write(text)
    return changed


def skin_names():
    """Lower-case words of the skin XML and the addon Python: a skin setting named in none of them is dead."""
    words = set()
    for pattern in ('resources/skin.milionar/xml/*.xml', 'wcs/**/*.py'):
        for path in glob.glob(os.path.join(ROOT, pattern), recursive=True):
            with open(path, encoding='utf-8') as handle:
                words.update(re.findall(r'\w+', handle.read().lower()))
    return words


def skin_defaults():
    user = read_settings(SKIN_SETTINGS)
    slots = sorted({(m.group(1), int(m.group(2))) for sid in user for m in [SLOT_KEY.match(sid)] if m})
    gate = re.search(r'!Skin\.HasSetting\((\w+)\)[^\n]*action=skin_defaults', open(HOME_XML, encoding='utf-8').read()).group(1).lower()
    used = skin_names()
    flags = {sid: value == 'true' for sid, (kind, value) in sorted(user.items())
             if kind == 'bool' and sid != gate and sid in used}   # the gate is set by the apply itself; flags the skin no longer reads are left behind
    strings = {sid: value for sid, (kind, value) in sorted(user.items())
               if kind == 'string' and not SLOT_KEY.match(sid) and not SECTION_FLAG.match(sid) and not SKIP_STRINGS.search(sid)}
    sections = {}
    for sec in sorted({sec for sec, _ in slots}):
        widgets = []
        for number in (n for s_, n in slots if s_ == sec):
            get = lambda key: user.get('mil.{}.{}.{}'.format(sec, number, key), ('string', ''))[1]
            if get('type') in ('', 'off'):
                continue
            widget = {key: get(key) for key in SLOT_FIELDS}
            for key in ('menu', 'hero'):
                widget[key] = json.loads(unquote(get(key))) if get(key) else None
            widgets.append(widget)
        flag = lambda key: user.get('mil.{}.{}'.format(sec, key), ('string', ''))[1] == '1'
        sections[sec] = {'widgets': widgets, 'hidewatched': flag('hidewatched'), 'upcoming': flag('upcoming')}
    with open(SKIN_JSON, encoding='utf-8') as handle:
        menus = json.load(handle).get('context_menu_profiles', {})
    saved_menus = read_settings(ADDON_SETTINGS).get('context_menu_profiles', ('string', ''))[1]
    if saved_menus:
        menus.update(json.loads(saved_menus))
    return {'flags': flags, 'strings': strings, 'sections': sections, 'context_menu_profiles': menus}


def main():
    check = '--check' in sys.argv
    values = addon_defaults()
    changed = write_addon_defaults(values, check)
    print('{} addon defaults, {} changed{}'.format(len(values), len(changed), ' (check only)' if check else ''))
    for sid, old, new in changed:
        print('  {}: {!r} -> {!r}'.format(sid, old, new))
    skin = skin_defaults()
    print('skin: {} flags, {} strings, {} widgets'.format(
        len(skin['flags']), len(skin['strings']), sum(len(s['widgets']) for s in skin['sections'].values())))
    if not check:
        os.makedirs(os.path.dirname(SKIN_JSON), exist_ok=True)
        with open(SKIN_JSON, 'w', encoding='utf-8') as handle:
            json.dump(skin, handle, ensure_ascii=False, indent=1, sort_keys=False)
            handle.write('\n')


if __name__ == '__main__':
    main()
