# Copyright 2004-2026 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from __future__ import division, absolute_import, with_statement, print_function, unicode_literals
from renpy.compat import PY2, basestring, bchr, bord, chr, open, pystr, range, round, str, tobytes, unicode  # *

from typing import Optional

import renpy
import time
import collections

import datetime

# Profiling ####################################################################

profile_log = renpy.log.open("profile_screen", developer=True, append=False, flush=False)

# A map from screen name to ScreenProfile object.
profile = {}


class ScreenProfile(renpy.object.Object):
    """
    :doc: profile_screen
    :name: renpy.profile_screen

    """

    def __init__(
        self, name, predict=False, show=False, update=False, request=False, time=False, debug=False, const=False
    ):
        """
        Requests screen profiling for the screen named `name`, which
        must be a string.

        Apart from `name`, all arguments must be supplied as keyword
        arguments. This function takes three groups of arguments.


        The first group of arguments determines when profiling occurs.

        `predict`
            If true, profiling occurs when the screen is being predicted.

        `show`
            If true, profiling occurs when the screen is first shown.

        `update`
            If true, profiling occurs when the screen is updated.

        `request`
            If true, profiling occurs when requested by pressing F8.

        The second group of arguments controls what profiling output is
        produced when profiling occurs.

        `time`
            If true, Ren'Py will log the amount of time it takes to evaluate
            the screen.

        `debug`
            If true, Ren'Py will log information as to how screens are
            evaluated, including:

            * Which displayables Ren'Py considers constant.
            * Which arguments, if any, needed to be evaluated.
            * Which displayables were reused.

            Producing and saving this debug information takes a noticeable
            amount of time, and so the `time` output should not be considered
            reliable if `debug` is set.

        The last group of arguments controls what output is produced once
        per Ren'Py run.

        `const`
            Displays the variables in the screen that are marked as const and
            not-const.

        All profiling output will be logged to profile_screen.txt in the game
        directory.
        """

        self.predict = predict
        self.show = show
        self.update = update
        self.request = request

        self.time = time
        self.debug = debug

        self.const = const

        if name is not None:
            if isinstance(name, str):
                name = tuple(name.split())
                profile[name] = self


def get_profile(name):
    """
    Returns the profile object for the screen with `name`, or a default
    profile object if none exists.

    `name`
        A string or tuple.
    """

    if isinstance(name, str):
        name = tuple(name.split())

    if name in profile:
        return profile[name]
    else:
        return ScreenProfile(None)


# Cache ########################################################################


# A map from screen name to a list of ScreenCache objects. We ensure the cache
# does not exceed config.screen_cache_size for each screen.
predict_cache = collections.defaultdict(list)


class ScreenCache(object):
    """
    Represents an entry in the screen cache. Upon creation, puts itself into
    the screen cache.
    """

    def __init__(self, screen, args, kwargs, cache):
        if screen.ast is None:
            return

        self.args = args
        self.kwargs = kwargs
        self.cache = cache

        pc = predict_cache[screen]

        pc.append(self)

        if len(pc) > renpy.config.screen_cache_size:
            pc.pop(0)


cache_put = ScreenCache


def cache_get(screen, args, kwargs):
    """
    Returns the cache to use when `screen` is accessed with `args` and
    `kwargs`.
    """

    if screen.ast is None:
        return {}

    pc = predict_cache[screen]

    if not pc:
        return {}

    for sc in pc:
        # Reuse w/ same arguments.
        if sc.args == args and sc.kwargs == kwargs:
            pc.remove(sc)
            break
    else:
        # Reuse the oldest.
        sc = pc.pop(0)

    return sc.cache


# Screens #####################################################################
class ScreenNotFound(LookupError):
    """
    Exception that is raised when a screen with given name does not exists.
    """

    def __init__(self, name: str):
        super().__init__(f"Screen {name!r} is not known.")
        self.name = name

    def get_suggestion(self):
        d = list(renpy.display.screen.screens_by_name)

        if self.name[:1] != "_":
            d = [x for x in d if x[:1] != "_"]

        if suggestion := renpy.error.compute_closest_value(self.name, d):
            return f" Did you mean: '{suggestion}'?"


class Screen(renpy.object.Object):
    """
    A screen is a collection of widgets that are displayed together.
    This class stores information about the screen.
    """

    sensitive = "True"
    roll_forward = None
    docstring = None

    def __init__(
        self,
        name,
        function,
        modal="False",
        zorder="0",
        tag=None,
        predict=None,
        variant=None,
        parameters=False,
        location=None,
        layer="screens",
        sensitive="True",
        roll_forward=None,
        docstring=None,
    ):
        # The name of this screen.
        if isinstance(name, str):
            name = tuple(name.split())

        self.name = name

        if variant is None or isinstance(variant, str):
            variant = [variant]

        for v in variant:
            screens[name[0], v] = self
            screens_by_name[name[0]][v] = self

        # A function that can be called to display the screen.
        self.function = function

        # If this is a SL2 screen, the SLScreen node at the root of this
        # screen.
        if isinstance(function, renpy.sl2.slast.SLScreen):
            self.ast = function
        else:
            self.ast = None

        # Expression: Are we modal? (A modal screen ignores screens under it.)
        self.modal = modal

        # Expression: Our zorder.
        self.zorder = zorder

        # The tag associated with the screen.
        self.tag = tag or name[0]

        # Can this screen be predicted?
        if predict is None:
            predict = renpy.config.predict_screens

        self.predict = predict

        # True if this screen takes parameters via _args and _kwargs.
        self.parameters = parameters

        # The location (filename, linenumber) of this screen.
        self.location = location

        # The layer the screen will be shown on.
        self.layer = layer

        # Is this screen sensitive? An expression.
        self.sensitive = sensitive

        # Does the screen participate in roll_forward when used with the
        # call screen statement? True for yes, False for no, None for
        # config.call_screen_roll_forward.
        self.roll_forward = roll_forward

        # The docstring for this screen.
        self.docstring = docstring

        global prepared
        global analyzed

        prepared = False
        analyzed = False


# Phases we can be in.
PREDICT = 0  # Predicting the screen before it is shown.
SHOW = 1  # Showing the screen for the first time.
UPDATE = 2  # Showing the screen for the second and later times.
HIDE = 3  # After the screen has been hid with "hide screen" (or the end of call screen).
OLD = 4  # A copy of the screen in the old side of a transition.

phase_name = [
    "PREDICT",
    "SHOW",
    "UPDATE",
    "HIDE",
    "OLD",
]


class ScreenDisplayable(renpy.display.layout.Container):
    """
    A screen is a collection of widgets that are displayed together. This
    class is responsible for managing the display of a screen.
    """

    nosave = [
        "screen",
        "child",
        "children",
        "transforms",
        "widgets",
        "base_widgets",
        "old_widgets",
        "hidden_widgets",
        "old_transforms",
        "cache",
        "miss_cache",
        "profile",
        "phase",
        "use_cache",
        "copied_from",
    ]

    noreach = [
        "screen",
        # 'child' needs to be reachable to keep the screen roll-backable.
        "children",
        "transforms",
        "widgets",
        "base_widgets",
        "old_widgets",
        "hidden_widgets",
        "old_transforms",
        "cache",
        "miss_cache",
        "profile",
        "phase",
        "use_cache",
        "copied_from",
    ]

    restarting = False
    hiding = False
    transient = False

    def after_setstate(self):
        self.screen = get_screen_variant(self.screen_name[0])
        self.child = None
        self.children = []
        self.transforms = {}
        self.widgets = {}
        self.base_widgets = {}
        self.old_widgets = None
        self.old_transforms = None
        self.old_transfers = False
        self.hidden_widgets = {}
        self.cache = {}
        self.phase = UPDATE
        self.use_cache = {}
        self.miss_cache = {}
        self.copied_from = None

        self.profile = profile.get(self.screen_name, None)

    def __init__(self, screen, tag, layer, widget_properties={}, scope={}, transient=False, **properties):
        super(ScreenDisplayable, self).__init__(**properties)

        # Stash the properties, so we can re-create the screen.
        self.properties = properties

        # The screen, and it's name. (The name is used to look up the
        # screen on save.)
        self.screen = screen
        self.screen_name = screen.name

        self._location = self.screen.location

        # The profile object that determines when we profile.
        self.profile = profile.get(self.screen_name, None)

        # The tag and layer screen was displayed with.
        self.tag = tag
        self.layer = layer

        # The scope associated with this statement. This is passed in
        # as keyword arguments to the displayable.
        self.scope = renpy.revertable.RevertableDict(scope)

        # The child associated with this screen.
        self.child = None  # type: renpy.display.layout.MultiBox|None

        # Widget properties given to this screen the last time it was
        # shown.
        self.widget_properties = widget_properties

        # A map from name to the widget with that name.
        self.widgets = {}

        # Same, but to the widget without considering _main.
        self.base_widgets = {}

        # The persistent cache.
        self.cache = {}

        if tag and layer:
            old_screen = get_screen(tag, layer, tag_only=True)
        else:
            old_screen = None

        # A map from name to the transform with that name. (This is
        # taken from the old version of the screen, if it exists.
        if old_screen is not None:
            self.transforms = old_screen.transforms
        else:
            self.transforms = {}

        # A map from a (screen name, id) pair to cache. This is for use
        # statements with the id parameter.
        if old_screen is not None:
            self.use_cache = old_screen.use_cache
        else:
            self.use_cache = {}

        # A version of the cache that's used when we have a screen that is
        # being displayed with the same tag with a cached copy of the screen
        # we want to display.
        self.miss_cache = {}

        # What widgets and transforms were the last time this screen was
        # updated. Used to communicate with the ui module, and only
        # valid during an update - not used at other times.
        self.old_widgets = None
        self.old_transforms = None

        # Should we transfer data from the old_screen? This becomes
        # true once this screen finishes updating for the first time,
        # and also while we're using something.
        self.old_transfers = old_screen and old_screen.screen_name == self.screen_name

        # The current transform event, and the last transform event to
        # be processed.
        self.current_transform_event = None

        # A dict-set of widgets (by id) that have been hidden from us.
        self.hidden_widgets = {}

        # Are we restarting or hiding?
        self.restarting = False
        self.hiding = False

        # Is this a transient screen?
        self.transient = transient

        # Modal and zorder.
        self.modal = renpy.python.py_eval(self.screen.modal, locals=self.scope)
        self.zorder = renpy.python.py_eval(self.screen.zorder, locals=self.scope)

        # The lifecycle phase we are in - one of PREDICT, SHOW, UPDATE, or HIDE.
        self.phase = PREDICT

        # If this screen was copied from another by _in_current_store, returns
        # the screen it was copied from.
        self.copied_from = None

    @property
    def name(self):
        return " ".join(self.screen_name)

    def _repr_info(self):
        rv = self.name
        p = self.screen.parameters
        if p is not None:
            rv += str(p)
        return rv

    def visit(self):
        return [self.child]

    def visit_all(self, callback, seen=None):
        callback(self)

        try:
            push_current_screen(self)
            if self.child is not None:
                self.child.visit_all(callback, seen=None)
        finally:
            pop_current_screen()

    def per_interact(self):
        renpy.display.render.redraw(self, 0)
        self.update()

    def set_transform_event(self, event):
        try:
            push_current_screen(self)
            super(ScreenDisplayable, self).set_transform_event(event)
            self.current_transform_event = event
        finally:
            pop_current_screen()

    def find_focusable(self, callback, focus_name):
        hiding = (self.phase == OLD) or (self.phase == HIDE)

        if self.modal and not callable(self.modal) and not hiding:
            renpy.display.focus.mark_modal()

        try:
            push_current_screen(self)

            if self.child and not hiding:
                self.child.find_focusable(callback, focus_name)
        finally:
            pop_current_screen()

    def copy(self):
        rv = ScreenDisplayable(self.screen, self.tag, self.layer, self.widget_properties, self.scope, **self.properties)
        rv.transforms = self.transforms.copy()
        rv.widgets = self.widgets.copy()
        rv.base_widgets = self.widgets.copy()
        rv.old_transfers = True
        rv.child = self.child

        return rv

    def _handles_event(self, event):
        if self.child is None:
            if self.transient:
                return False

            self.update()

        return self.child._handles_event(event)

    def _hide(self, st, at, kind):
        if kind == "cancel" and renpy.config.screens_never_cancel_hide:
            return self

        if self.phase == HIDE:
            hid = self
        else:
            if (self.child is not None) and (not self.child._handles_event(kind)):
                return None

            updated_screens.discard(self)
            self.update()

            if self.screen is None:
                return None

            if self.child is None:
                return None

            if not self.child._handles_event(kind):
                return None

            if self.screen.ast is not None:
                self.screen.ast.copy_on_change(self.cache.get(0, {}))

            hid = self.copy()

            try:
                push_current_screen(self)

                for i in self.child.children:
                    i.set_transform_event(kind)
            finally:
                pop_current_screen()

            if self.modal:
                renpy.display.render.redraw(self, 0)

        hid.phase = HIDE
        hid.copied_from = self

        rv = None

        old_child = hid.child

        if not isinstance(old_child, renpy.display.layout.MultiBox):
            return None

        renpy.ui.detached()
        hid.child = renpy.ui.default_fixed(focus="_screen_" + "_".join(self.screen_name))
        hid.children = [hid.child]
        renpy.ui.close()

        for d in old_child.children:
            c = d._hide(st, at, kind)

            if c is not None:
                renpy.display.render.redraw(c, 0)
                hid.child.add(c)

                rv = hid

        if hid is not None:
            renpy.display.render.redraw(hid, 0)

        return rv

    def _in_current_store(self):
        if self.screen is None:
            return self

        if self.child is None:
            return self

        if not renpy.config.transition_screens:
            return self

        if self.screen.ast is not None:
            self.screen.ast.copy_on_change(self.cache.get(0, {}))

        rv = self.copy()
        rv.phase = OLD
        rv.child = self.child._in_current_store()
        rv.copied_from = self

        return rv

    def update(self):
        if self in updated_screens:
            return

        updated_screens.add(self)

        if self.screen is None:
            self.child = renpy.display.layout.MultiBox(layout="fixed")
            return {}

        # Do not update if restarting or hiding.
        if self.restarting or (self.phase == HIDE) or (self.phase == OLD):
            if not self.child:
                self.child = renpy.display.layout.MultiBox(layout="fixed")

            return self.widgets

        profile = False
        debug = False

        if self.profile:
            if self.phase == UPDATE and self.profile.update:
                profile = True
            elif self.phase == SHOW and self.profile.show:
                profile = True
            elif self.phase == PREDICT and self.profile.predict:
                profile = True

            if renpy.display.interface.profile_once and self.profile.request:
                profile = True

            if profile:
                profile_log.write(
                    "%s %s %s",
                    phase_name[self.phase],
                    " ".join(self.screen_name),
                    datetime.datetime.now().strftime("%H:%M:%S.%f"),
                )

                start = time.time()

                if self.profile.debug:
                    debug = True

        # Cycle widgets and transforms.
        self.old_widgets = self.widgets
        self.old_transforms = self.transforms
        self.widgets = {}
        self.base_widgets = {}
        self.transforms = {}

        push_current_screen(self)

        old_ui_screen = renpy.ui.screen
        renpy.ui.screen = self

        # The name of the root screen of this screen.
        NAME = 0

        old_cache = self.cache.get(NAME, None)

        # Evaluate the screen.
        try:
            renpy.ui.detached()
            self.child = renpy.ui.default_fixed(focus="_screen_" + "_".join(self.screen_name))
            self.children = [self.child]

            self.scope["_scope"] = self.scope
            self.scope["_name"] = NAME
            self.scope["_debug"] = debug

            self.screen.function(**self.scope)

            renpy.ui.close()

        finally:
            # Safe removal as to not reraise another exception and lose the last one
            self.scope.pop("_scope", None)

            renpy.ui.screen = old_ui_screen
            pop_current_screen()

        # Finish up.
        self.old_widgets = None
        self.old_transforms = None
        self.old_transfers = True

        if self.miss_cache:
            self.miss_cache.clear()

        # Send a pending transform event.

        if self.current_transform_event:
            try:
                push_current_screen(self)

                for i in self.child.children:
                    i.set_transform_event(self.current_transform_event)

            finally:
                pop_current_screen()

            self.current_transform_event = None

        if profile:
            end = time.time()

            if self.profile.time:
                profile_log.write("* %.2f ms", 1000 * (end - start))  # type: ignore

            if self.profile.debug:
                profile_log.write("\n")

        if self.phase == SHOW:
            self.phase = UPDATE

        return self.widgets

    def render(self, w, h, st, at):
        if not self.child:
            self.update()
        try:
            push_current_screen(self)
            child = renpy.display.render.render(self.child, w, h, st, at)
        finally:
            pop_current_screen()

        rv = renpy.display.render.Render(w, h)
        rv.focus_screen = self

        hiding = (self.phase == OLD) or (self.phase == HIDE)

        if self.screen is None:
            sensitive = False
        else:
            sensitive = renpy.python.py_eval(self.screen.sensitive, locals=self.scope)

        rv.blit(child, (0, 0), focus=sensitive and not hiding, main=not hiding)
        rv.modal = self.modal and not hiding

        return rv

    def get_placement(self):
        if not self.child:
            self.update()

        return self.child.get_placement()

    def event(self, ev, x, y, st):
        if (self.phase == OLD) or (self.phase == HIDE):
            return

        if not self.screen:
            return None

        if not renpy.python.py_eval(self.screen.sensitive, locals=self.scope):
            ev = renpy.display.interface.time_event

        try:
            push_current_screen(self)

            rv = self.child.event(ev, x, y, st)
        finally:
            pop_current_screen()

        if rv is not None:
            return rv

        if renpy.display.layout.check_modal(self.modal, ev, x, y, None, None):
            raise renpy.display.layout.IgnoreLayers()

    def get_phase_name(self):
        return phase_name[self.phase]

    def _tts(self, raw: bool) -> str:
        if (self.phase == OLD) or (self.phase == HIDE):
            return ""

        if self.modal:
            return renpy.display.tts.TTSDone(self._tts_common(raw=raw))
        else:
            return self._tts_common(raw=raw)


# The name of the screen that is currently being displayed, or
# None if no screen is being currently displayed.
_current_screen = None  # type: renpy.display.screen.ScreenDisplayable|None

if 0 == 1:
    _current_screen = renpy.display.screen.ScreenDisplayable()  # type: ignore # fake out typing.
else:
    _current_screen = None

# The stack of old current screens.
current_screen_stack = []


def push_current_screen(screen):
    global _current_screen
    current_screen_stack.append(_current_screen)
    _current_screen = screen


def pop_current_screen():
    global _current_screen
    _current_screen = current_screen_stack.pop()


# A map from (screen_name, variant) tuples to screen.
screens = {}

# A map from screen name to map from variant to screen.
screens_by_name = collections.defaultdict(dict)

# The screens that were updated during the current interaction.
updated_screens = set()


def get_screen_variant(name, candidates=None):
    """
    Get a variant screen object for `name`.

    `candidates`
        A list of candidate variants.
    """

    if candidates is None:
        candidates = renpy.config.variants

    for i in candidates:
        rv = screens.get((name, i), None)
        if rv is not None:
            return rv

    return None


def get_all_screen_variants(name):
    """
    Gets all variants of the screen with `name`.

    Returns a list of (`variant`, `screen`) tuples, in no particular
    order.
    """

    if isinstance(name, str):
        name = tuple(name.split())

    name = name[0]

    if name not in screens_by_name:
        return []

    return list(screens_by_name[name].items())


# Have all screens been analyzed?
analyzed = False

# Have the screens been prepared?
prepared = False

# Caches for sort_screens.
sorted_screens = []
screens_at_sort = {}

# The list of screens that participate in a use cycle.
use_cycle = []


def sort_screens():
    """
    Produces a list of SL2 screens in topologically sorted order.
    """

    global use_cycle
    global sorted_screens
    global screens_at_sort

    if screens_at_sort == screens:
        return sorted_screens

    # For each screen, the set of screens it uses.
    depends = collections.defaultdict(set)

    # For each screen, the set of screens that use it.
    reverse = collections.defaultdict(set)

    names = {i[0] for i in screens}

    for k, v in screens.items():
        name = k[0]

        # Ensure name exists.
        depends[name]

        if not v.ast:
            continue

        def callback(uses):
            if uses not in names:
                return

            depends[name].add(uses)
            reverse[uses].add(name)

        v.ast.used_screens(callback)

    rv = []

    workset = {k for k, v in depends.items() if not len(v)}

    while workset:
        name = workset.pop()
        rv.append(name)

        for i in reverse[name]:
            d = depends[i]
            d.remove(name)

            if not d:
                workset.add(i)

        del reverse[name]

    # Store the use cycle for later reporting.
    use_cycle = list(reverse.keys())
    use_cycle.sort()

    sorted_screens = rv
    screens_at_sort = dict(screens)

    return rv


def sorted_variants():
    """
    Produces a list of screen variants in topological order.
    """

    rv = []

    for name in sort_screens():
        rv.extend(screens_by_name[name].values())

    return rv


def analyze_screens():
    """
    Analyzes all screens.
    """

    global analyzed

    if analyzed:
        return

    for s in sorted_variants():
        if s.ast is None:
            continue

        s.ast.analyze_screen()

    analyzed = True


def prepare_screens():
    """
    Prepares all screens for use.
    """

    global prepared

    if prepared:
        return

    predict_cache.clear()

    old_predicting = renpy.display.predict.predicting
    renpy.display.predict.predicting = True

    try:
        if not analyzed:
            analyze_screens()

        for s in sorted_variants():
            if s.ast is None:
                continue

            s.ast.unprepare_screen()
            s.ast.prepare_screen()

        # Compile ATL in screens.
        renpy.atl.compile_all()

        prepared = True

    finally:
        renpy.display.predict.predicting = old_predicting

    if renpy.config.developer and use_cycle:
        raise Exception(
            "The following screens use each other in a loop: " + ", ".join(use_cycle) + ". This is not allowed."
        )


def define_screen(*args, **kwargs):
    """
    :undocumented:
    :args: (name, function, modal="False", zorder="0", tag=None, variant=None)

    Defines a screen with `name`, which should be a string.

    `function`
        The function that is called to display the screen. The
        function is called with the screen scope as keyword
        arguments. It should ignore additional keyword arguments.

        The function should call the ui functions to add things to the
        screen.

    `modal`
        A string that, when evaluated, determines of the created
        screen should be modal. A modal screen prevents screens
        underneath it from receiving input events.

    `zorder`
        A string that, when evaluated, should be an integer. The integer
        controls the order in which screens are displayed. A screen
        with a greater zorder number is displayed above screens with a
        lesser zorder number.

    `tag`
        The tag associated with this screen. When the screen is shown,
        it replaces any other screen with the same tag. The tag
        defaults to the name of the screen.

    `predict`
        If true, this screen can be loaded for image prediction. If false,
        it can't. Defaults to true.

    `variant`
        String. Gives the variant of the screen to use.
    """

    Screen(*args, **kwargs)


def get_screen_layer(name):
    """
    Returns the layer that the screen with `name` is part of.
    """

    if not isinstance(name, str):
        name = name[0]

    screen = get_screen_variant(name)

    if screen is None:
        return "screens"
    else:
        return screen.layer


def get_screen_docstring(name, variant=None):
    """
    :doc: screens

    Returns the docstring for the screen with `name` and `variant`.

    `name`
        The name of the screen.
    `variant`
        The variant of the screen. If None, the default variant is used.
    """

    screen = get_screen_variant(name, [variant])
    if screen is not None:
        return screen.docstring
    else:
        return None


def get_screen(name, layer=None, tag_only=False):
    """
    :doc: screens

    Returns information about the screen with the given `name` on `layer`.
    `name` is first interpreted as a tag name, and then as a screen name.
    If the screen is not showing, returns None.

    This can also take a list of names, in which case the first screen
    that is showing is returned.

    `tag_only`
        If true, only the tag is considered.

    This function can be used to check whether a screen is showing::

        if renpy.get_screen("say"):
            text "The say screen is showing."
        else:
            text "The say screen is hidden."

    The objects returned by this function have the
    following documented fields:

    .. attribute:: layer

        The layer the screen is being displayed on.

    .. attribute:: name

        The name of the screen.

    .. attribute:: zorder

        The zorder the screen is being displayed at.

    .. warning::

        Like other similar functions, the object this returns is meant to be used
        in the short term after the function is called. Including it in save data
        or making it participate in rollback is not advised.
    """

    if layer is None:
        layer = get_screen_layer(name)

    if isinstance(name, str):
        name = (name,)

    sl = renpy.exports.scene_lists()

    for tag in name:
        sd = sl.get_displayable_by_tag(layer, tag)
        if sd is not None:
            return sd

    if not tag_only:
        for tag in name:
            sd = sl.get_displayable_by_name(layer, (tag,))
            if sd is not None:
                return sd

    return None


def get_screen_variable(name, screen=None, layer=None):
    """
    :doc: screens
    :args: (name, *, screen=None, layer=None)

    Returns the value of a variable in the scope of a screen.

    `name`
        The name of the variable to return.

    `screen`
        The name of the screen to return the variable from. If None,
        the current screen is used. (The current screen is only defined
        when updating a screen, and in actions that are run inside the
        screen.)

    `layer`
        The layer to find the screen on, if `screen` is not None.
    """

    if screen is None:
        s = current_screen()
        if s is None:
            raise ValueError("There is no current screen.")

    else:
        s = get_screen(screen, layer)
        if s is None:
            raise ValueError("Screen %s is not showing." % (screen,))

    if name not in s.scope:
        raise NameError("Screen %s does not have a variable named %s." % (s.name, name))

    return s.scope[name]


def set_screen_variable(name, value, screen=None, layer=None):
    """
    :doc: screens
    :args: (name, value, *, screen=None, layer=None)

    Sets a variable to a value in the scope of a screen. Note that this
    will not immediately update the variable's value - call
    :func:`renpy.restart_interaction` to cause the screen to be updated.

    `name`
        The name of the variable to set. This should be a variable
        created with the :ref:`sl-default` statement, as optimization
        may prevent changes to other variables from being visible.

    `value`
        The value to set the variable to.

    `screen`
        The name of the screen to return the variable from. If None,
        the current screen is used. (The current screen is only defined
        when updating a screen, and in actions that are run inside the
        screen.)

    `layer`
        The layer to find the screen on, if `screen` is not None.
    """
    if screen is None:
        s = current_screen()
        if s is None:
            raise ValueError("There is no current screen.")

    else:
        s = get_screen(screen, layer)
        if s is None:
            raise ValueError("Screen %s is not showing." % (screen,))

    s.scope[name] = value


def has_screen(name):
    """
    Returns true if a screen with the given name exists.
    """

    if not isinstance(name, tuple):
        name = tuple(name.split())

    if not name:
        return False

    if get_screen_variant(name[0]):
        return True
    else:
        return False


def get_screen_roll_forward(screen_name):
    """
    Given a screen name, determines if roll forward is enable for the
    screen.
    """

    name = screen_name

    if not isinstance(name, tuple):
        name = tuple(name.split())

    screen = get_screen_variant(name[0])

    if screen is None:
        raise ScreenNotFound(name[0])

    return screen.roll_forward


def show_screen(_screen_name, *_args, **kwargs):
    """
    :doc: screens
    :args: (_screen_name, *args, _layer=None, _zorder=None, _tag=None, _widget_properties={}, _transient=False, **kwargs)

    The programmatic equivalent of the show screen statement.

    Shows the named screen. This takes the following keyword arguments:

    `_screen_name`
        The name of the screen to show.
    `_layer`
        The layer to show the screen on. This is equivalent to the
        ``onlayer`` clause of the :ref:`show-screen-statement` statement.
    `_zorder`
        The zorder to show the screen on. If not specified, defaults to
        the zorder associated with the screen. If that's not specified,
        it is 0 by default.
    `_tag`
        The tag to show the screen with. If not specified, defaults to
        the tag associated with the screen. If that's not specified,
        defaults to the name of the screen.

        This is equivalent to the ``as`` clause of the
        :ref:`show-screen-statement` statement.
    `_widget_properties`
        A map from the id of a widget to a property name -> property value
        dictionary. When a widget with that id is shown by the screen, the
        specified properties are added to it.
    `_transient`
        If true, the screen will be automatically hidden at the end of
        the current interaction.

    Non-keyword arguments, and keyword arguments that do not begin with
    an underscore, are passed to the screen.
    """

    _layer = kwargs.pop("_layer", None)
    _tag = kwargs.pop("_tag", None)
    _widget_properties = kwargs.pop("_widget_properties", {})
    _transient = kwargs.pop("_transient", False)
    _zorder = kwargs.pop("_zorder", None)

    name = _screen_name

    if not isinstance(name, tuple):
        name = tuple(name.split())

    screen = get_screen_variant(name[0])

    if screen is None:
        raise ScreenNotFound(name[0])

    if _layer is None:
        _layer = get_screen_layer(name)

    if _tag is None:
        _tag = screen.tag

    scope = {}

    if screen.parameters:
        scope["_kwargs"] = kwargs
        scope["_args"] = _args
    else:
        scope.update(kwargs)

    d = ScreenDisplayable(screen, _tag, _layer, _widget_properties, scope, transient=_transient)

    if _zorder is None:
        _zorder = d.zorder

    old_d = get_screen(_tag, _layer, tag_only=True)

    if old_d and old_d.cache:
        d.cache = old_d.cache
        d.miss_cache = cache_get(screen, _args, kwargs)
        d.phase = UPDATE
    else:
        d.cache = cache_get(screen, _args, kwargs)
        d.phase = SHOW

    sls = renpy.display.scenelists.scene_lists()

    sls.add(_layer, d, _tag, zorder=_zorder, transient=_transient, keep_st=True, name=name)
    if not _transient:
        sls.shown.predict_show(_layer, name, True)


def predict_screen(_screen_name, *_args, **kwargs):
    """
    Predicts the displayables that make up the given screen.

    `_screen_name`
        The name of the  screen to show.
    `_widget_properties`
        A map from the id of a widget to a property name -> property
        value map. When a widget with that id is shown by the screen,
        the specified properties are added to it.

    Keyword arguments not beginning with underscore (_) are used to
    initialize the screen's scope.
    """

    _layer = kwargs.pop("_layer", None)
    _tag = kwargs.pop("_tag", None)
    _widget_properties = kwargs.pop("_widget_properties", {})
    _transient = kwargs.pop("_transient", False)
    _zorder = kwargs.pop("_zorder", None)

    name = _screen_name

    if renpy.config.debug_prediction:
        renpy.display.ic_log.write("Predict screen %s", name)

    if not isinstance(name, tuple):
        name = tuple(name.split())

    screen = get_screen_variant(name[0])

    if screen is None:
        return

    if not screen.predict:
        return

    if _layer is None:
        _layer = get_screen_layer(name)

    scope = {}
    scope["_scope"] = scope

    if screen.parameters:
        scope["_kwargs"] = kwargs
        scope["_args"] = _args
    else:
        scope.update(kwargs)

    try:
        d = ScreenDisplayable(screen, None, None, _widget_properties, scope)
        d.cache = cache_get(screen, _args, kwargs)
        d.update()
        cache_put(screen, _args, kwargs, d.cache)

        renpy.display.predict.displayable(d)

    except Exception:
        if renpy.config.debug_prediction:
            import traceback

            print("While predicting screen", _screen_name)
            traceback.print_exc()
            print()

    finally:
        scope.pop("_scope", None)

    renpy.ui.reset()


def hide_screen(tag, layer=None, immediately=False):
    """
    :doc: screens

    The programmatic equivalent of the hide screen statement.

    Hides the screen with `tag` on `layer`.

    If `immediately` is true, the screen is hidden immediately, without the 'on hide' event.
    """

    if layer is None:
        layer = get_screen_layer((tag,))

    screen = get_screen(tag, layer)

    sls = renpy.display.scenelists.scene_lists()

    if screen is not None:
        sls.remove(layer, screen.tag, prefix="hide" if not immediately else None)
        sls.shown.predict_hide(layer, screen.screen_name)


def use_screen(_screen_name, *_args, **kwargs):
    _name = kwargs.pop("_name", ())
    _scope = kwargs.pop("_scope", {})

    name = _screen_name

    if not isinstance(name, tuple):
        name = tuple(name.split())

    screen = get_screen_variant(name[0])

    if screen is None:
        raise ScreenNotFound(name[0])

    old_transfers = _current_screen.old_transfers
    _current_screen.old_transfers = True

    if screen.parameters:
        scope = {}
        scope["_kwargs"] = kwargs
        scope["_args"] = _args
    else:
        scope = _scope.copy()
        scope.update(kwargs)

    scope["_scope"] = scope
    scope["_name"] = (_name, name)

    try:
        screen.function(**scope)
    finally:
        scope.pop("_scope", None)

    _current_screen.old_transfers = old_transfers


def current_screen():  # type: () -> ScreenDisplayable|None
    """
    :doc: screens
    :name: renpy.current_screen

    Returns information about the screen currently being
    updated, rendered, or processed.

    See :func:`get_screen` for documented fields on the returned object.
    """

    if _current_screen and _current_screen.copied_from:
        return _current_screen.copied_from

    return _current_screen


def get_displayable(screen, id, layer=None, base=False):
    """
    :doc: screens
    :name: renpy.get_displayable

    From the `screen` on `layer`, returns the displayable with
    `id`. Returns None if the screen doesn't exist, or there is no
    widget with that id on the screen.
    """

    if isinstance(screen, ScreenDisplayable):
        screen = screen.screen_name

    if screen is None:
        screen = current_screen()
    else:
        if layer is None:
            layer = get_screen_layer(screen)

        screen = get_screen(screen, layer)

    if not isinstance(screen, ScreenDisplayable):
        return None

    if screen.child is None:
        screen.update()

    if base:
        rv = screen.base_widgets.get(id, None)
    else:
        rv = screen.widgets.get(id, None)

    return rv


get_widget = get_displayable


def get_displayable_properties(id, screen=None, layer=None):
    """
    :doc: screens
    :name: renpy.get_displayable_properties

    Returns the properties for the displayable with `id` in the `screen`
    on `layer`. If `screen` is None, returns the properties for the
    current screen. This can be used from Python or property code inside
    a screen.

    Note that this returns a dictionary containing the widget properties,
    and so to get an individual property, the dictionary must be accessed.
    """

    if screen is None:
        s = current_screen()
    else:
        if layer is None:
            layer = get_screen_layer(screen)

        s = get_screen(screen, layer)

    if s is None:
        return {}

    rv = s.widget_properties.get(id, None)

    if rv is None:
        rv = {}

    return rv


get_widget_properties = get_displayable_properties


def before_restart():
    """
    This is called before Ren'Py restarts to put the screens into restart
    mode, which prevents crashes due to variables being used that are no
    longer defined.
    """

    for k, layer in renpy.display.interface.old_scene.items():
        if k is None:
            continue

        for i in layer.children:
            if isinstance(i, ScreenDisplayable):
                i.restarting = True


def show_overlay_screens(suppress_overlay):
    """
    Called from interact to show or hide the overlay screens.
    """

    show = not suppress_overlay

    if renpy.store._overlay_screens is None:
        pass
    elif renpy.store._overlay_screens is True:
        show = True
    else:
        show = False

    if show:
        for i in renpy.config.overlay_screens:
            if get_screen(i) is None:
                show_screen(i)

    else:
        for i in renpy.config.overlay_screens:
            if get_screen(i) is not None:
                hide_screen(i)

    for i in renpy.config.always_shown_screens:
        if get_screen(i) is None and has_screen(i):
            show_screen(i)


def per_frame():
    """
    Called from interact once per frame to invalidate screens we want to
    update once per frame.
    """

    for i in renpy.config.per_frame_screens:
        s = get_screen(i)

        if s is None:
            continue

        updated_screens.discard(s)
        renpy.display.render.invalidate(s)
        s.update()
