# -*- coding: utf-8 -*-
"""Home loading order: the section on screen at once, every other section right afterwards, all kept current.

Kodi starts the fetch of every skin list when the skin loads (all ~40 `plugin://` rows at once), so the screen the
user looks at would queue behind them. Therefore the skin gives every section an empty content URL until
Window(Home).Property(milionar.go.<section>) is set (gen_slots.py, MilionarContent_*), or the user focuses that menu
item.

A row call is cheap: the service prepares every row beforehand (wcs/skin/journal.py) and the plugin call only hands
it over. This service thread
  1. waits until the section on screen has loaded, then loads the others off-screen, a few at a time (BATCH): it sets
     Window(Home).Property(milionar.warm.<menu id>), Home.xml then shows the group off-screen (alpha 0), Kodi runs the
     lists' fetches, and the property is cleared as soon as every list of the batch reports it is done;
  2. repeats that for every section whose row revision changed (refresh.publish -> request()), so rows are current
     before the user opens them, also after returning from playback.

Only runs while Home is the active window, nothing plays and no dialog is open, so it never competes with playback.

The section on screen always goes first: while its rows load, nothing is loaded in the background. A section that
needs longer than SLICE gives way to the others and continues after them. The gate of every section the user visits
is kept open, so its rows stay when the focus leaves. A section whose menu item is switched off is never loaded.
"""
import threading
import time

import xbmc
import xbmcgui
from wcs.skin import family, perflog, userfirst
from wcs.skin.hero_contract import WIDGET_SLOTS

# section -> (menu id of Home.xml, skin setting that hides the menu item, first list id of gen_slots.SECTIONS): menu order
SECTIONS = {
    'foryou': ('mil_foryou', 'HomeMenuNoMilForYouButton', 32000),
    'cont': ('mil_continue', 'HomeMenuNoMilContinueButton', 24000),
    'my_movies': ('mil_mymovies', 'HomeMenuNoMilMyMoviesButton', 29000),
    'my_series': ('mil_myseries', 'HomeMenuNoMilMySeriesButton', 30000),
    'mytv': ('mil_mytv', 'HomeMenuNoMilMyTVButton', 22000),
    'vinyl': ('mil_vinyl', 'HomeMenuNoMilVinylButton', 31000),
    'mycinema': ('mil_mycinema', 'HomeMenuNoMilMyCinemaButton', 23000),
    'game': ('mil_game', 'HomeMenuNoMilGameButton', 25000),
    'ai': ('mil_ai', 'HomeMenuNoMilAIButton', 26000),
    'kraska': ('mil_kraska', 'HomeMenuNoMilKraskaButton', 27000),
    'tmdb': ('mil_tmdb', 'HomeMenuNoMilTMDbButton', 28000),
}
BY_MENU = {values[0]: section for section, values in SECTIONS.items()}
SLOTS = WIDGET_SLOTS
BATCH = 3                            # sections loaded off-screen together (each shows its cards' images once)
MIN_DWELL = 0.3                      # a list needs a frame or two to start its fetch
STABLE_POLLS = 2                     # consecutive polls with no list updating
POLL = 0.1
GATE_SETTLE = 0.15                   # the skin must see the new URL before the group becomes visible
SLICE = 6.0                          # a section that still loads after this lets the others go first ...
MAX_SLICES = 4                       # ... and is given up after this many turns (slow network rows)
HIDDEN_POLL = 2.0                    # how often the "menu item switched off" settings are read
FIRST_SCREEN_CAP = 8.0
QUEUE_PROPERTY = 'milionar.warm.queue'
WARM_PROPERTY = 'milionar.warm.'     # + menu id: the group is shown off-screen
QUIET_PROPERTY = 'milionar.warm.quiet.'   # + menu id: its hide animation is skipped
QUIET_LEAD = 0.15                    # the skin must see the quiet flag before the group is hidden
QUIET_HOLD = 0.4                     # ... and keep it until the hide animation would have ended
GATE_PROPERTY = 'milionar.go.'
DONE, RETRY, LATER = True, False, 'later'


def request(sections):
    """Queue sections for a refresh; usable from any process (the service picks it up)."""
    sections = {s for s in sections if s in SECTIONS}
    if not sections:
        return
    home = xbmcgui.Window(10000)
    queued = set(filter(None, home.getProperty(QUEUE_PROPERTY).split(',')))
    home.setProperty(QUEUE_PROPERTY, ','.join(sorted(queued | sections)))


def _take_requests():
    home = xbmcgui.Window(10000)
    value = home.getProperty(QUEUE_PROPERTY)
    if value:
        home.clearProperty(QUEUE_PROPERTY)
    return [s for s in value.split(',') if s in SECTIONS]


def _list_ids(section):
    ids = []
    for number in range(1, SLOTS + 1):
        row = xbmc.getInfoLabel('Skin.String(mil.{}.{}.type)'.format(section, number))
        if row and row != 'off':
            ids.append(SECTIONS[section][2] + number * 100)
    return ids


def _updating(ids):
    return any(xbmc.getCondVisibility('Container({}).IsUpdating'.format(i)) for i in ids)


def _can_run():
    return (xbmc.getCondVisibility('Window.IsActive(home)') and not xbmc.getCondVisibility('Player.HasMedia')
            and not xbmc.getCondVisibility('System.HasActiveModalDialog')
            and not xbmcgui.Window(10000).getProperty('milionar.skin.installing')
            and family.active()
            and not userfirst.active())               # the user started something: that first


def _focused_section():
    return BY_MENU.get(xbmc.getInfoLabel('Container(9000).ListItem.Property(id)'))


def _open_gate(section):
    xbmcgui.Window(10000).setProperty(GATE_PROPERTY + section, '1')


def hidden(section):
    """The user switched the section's menu item off: nothing of it is loaded, anywhere."""
    return bool(xbmc.getCondVisibility('Skin.HasSetting({})'.format(SECTIONS[section][1])))


def _latch():
    """Keep the gate of the section on screen open (the skin opens it only while its menu item has the focus)."""
    focused = _focused_section()
    if focused:
        _open_gate(focused)
    return focused


def _first_screen_loaded(monitor):
    """Wait until the lists of the section on screen finished their first fetch (or a cap): nothing competes with them."""
    waited, stable = 0.0, 0
    while waited < FIRST_SCREEN_CAP and not monitor.abortRequested():
        if monitor.waitForAbort(POLL):
            return False
        waited += POLL
        if not xbmc.getCondVisibility('Window.IsActive(home)'):
            continue
        focused = _latch()
        if focused and _updating(_list_ids(focused)):
            stable = 0
        else:
            stable += 1
            if stable >= STABLE_POLLS and waited >= MIN_DWELL:
                break
    return True


def _warm(monitor, sections):
    """Load a batch of sections off-screen: {section: DONE (or pointless) / RETRY (interrupted: playback, another
    window, the section on screen loads) / LATER (its time slice is over and it still loads)}."""
    result, ids = {}, {}
    for section in sections:
        if hidden(section):
            result[section] = DONE
            continue
        _open_gate(section)
        ids[section] = _list_ids(section)
        if _focused_section() == section or not ids[section]:
            result[section] = DONE                      # the user is looking at it: it updates itself
    batch = [section for section in sections if section not in result]
    if not batch or monitor.waitForAbort(GATE_SETTLE):
        return {section: result.get(section, DONE) for section in sections}
    home = xbmcgui.Window(10000)
    for section in batch:
        home.setProperty(WARM_PROPERTY + SECTIONS[section][0], '1')
    started = time.monotonic()
    perflog.log('warm {} start'.format('+'.join(batch)))
    outcome = 'done'
    try:
        waited, stable, front, front_ids = 0.0, {section: 0 for section in batch}, None, []
        while waited < SLICE and len(result) < len(sections):
            if monitor.waitForAbort(POLL):
                break
            waited += POLL
            if not xbmc.getCondVisibility('Window.IsActive(home)') or xbmc.getCondVisibility('Player.HasMedia'):
                outcome = 'interrupted'
                break
            focused = _latch()
            if focused in batch:
                result.setdefault(focused, DONE)
            if focused != front:
                front, front_ids = focused, _list_ids(focused) if focused else []
            if focused not in batch and _updating(front_ids):
                outcome = 'gave way to ' + focused
                break
            if waited < MIN_DWELL:
                continue
            for section in batch:
                if section in result:
                    continue
                if _updating(ids[section]):
                    stable[section] = 0
                else:
                    stable[section] += 1
                    if stable[section] >= STABLE_POLLS:
                        result[section] = DONE
        else:
            if len(result) < len(sections):
                outcome = 'still loading'
        for section in batch:
            result.setdefault(section, LATER if outcome == 'still loading' else RETRY if outcome != 'done' else DONE)
        return result
    finally:
        # Hide the groups without their hide animation (Includes_Animations.xml: Visible_Right_Delayed_Warm), or they
        # would swipe across the screen
        for section in batch:
            home.setProperty(QUIET_PROPERTY + SECTIONS[section][0], '1')
        monitor.waitForAbort(QUIET_LEAD)
        for section in batch:
            home.clearProperty(WARM_PROPERTY + SECTIONS[section][0])
        monitor.waitForAbort(QUIET_HOLD)
        for section in batch:
            home.clearProperty(QUIET_PROPERTY + SECTIONS[section][0])
        perflog.log('warm {} end after {:.1f} s ({})'.format('+'.join(batch), time.monotonic() - started, outcome))


def run(monitor):
    """Service thread."""
    if not _first_screen_loaded(monitor):
        return
    pending = list(SECTIONS)                           # first pass: menu order
    slices = {}
    was_hidden = {section for section in SECTIONS if hidden(section)}
    checked = time.monotonic()
    while not monitor.abortRequested():
        focused = _latch()                              # the skin already does this on focus; make it permanent
        for section in _take_requests():               # changed sections go first
            if section in pending:
                pending.remove(section)
            pending.insert(0, section)
        if time.monotonic() - checked >= HIDDEN_POLL:
            checked = time.monotonic()
            now_hidden = {section for section in SECTIONS if hidden(section)}
            shown, was_hidden = was_hidden - now_hidden, now_hidden
            if shown:                                   # switched on again: its row revisions were not kept meanwhile
                from wcs.skin import refresh
                refresh.publish(warm_sections=False)
                pending = sorted(shown) + [section for section in pending if section not in shown]
        if pending and _can_run() and not (focused and _updating(_list_ids(focused))):
            # only sections that have something to load make up a batch
            batch = [section for section in pending
                     if not hidden(section) and section != focused and _list_ids(section)][:BATCH] or pending[:BATCH]
            for section in pending[:pending.index(batch[-1])]:
                if section not in batch:
                    batch.append(section)               # nothing to load: only its gate is opened
            for section, result in _warm(monitor, batch).items():
                if result is LATER:
                    pending.remove(section)
                    slices[section] = slices.get(section, 0) + 1
                    if slices[section] < MAX_SLICES:
                        pending.append(section)         # the others go first
                elif result:
                    pending.remove(section)
                    slices.pop(section, None)
            continue
        if monitor.waitForAbort(0.5):
            return


def _guarded(monitor):
    try:
        run(monitor)
    except Exception as exc:                          # a failure here must never be silent: the sections would stay empty
        xbmc.log('[Milionář Skin] warm thread stopped: {}: {}'.format(type(exc).__name__, exc), xbmc.LOGERROR)
        for section in SECTIONS:                       # every section still loads when it gets the focus, and stays
            _open_gate(section)


def start(monitor):
    threading.Thread(target=_guarded, args=(monitor,), name='milionar-warm', daemon=True).start()
